Restrict DNS access to VPN clients only

Security fix: The firewall rule for DNS was accepting traffic from any
source (0.0.0.0/0) to the local DNS resolver. While the service IP is
on the loopback interface (which normally isn't routable externally),
this could be a security risk if misconfigured.

Changed firewall rules to only accept DNS traffic from VPN subnets:
- INPUT rule now includes -s {{ subnets }} to restrict source IPs
- Applied to both IPv4 and IPv6 rules
- Added test to verify DNS is properly restricted

This ensures the DNS resolver is only accessible to connected VPN
clients, not the entire internet.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dan Guido 2025-08-17 19:19:00 -04:00
parent 15be88d28b
commit dbe8f23cdd
3 changed files with 34 additions and 4 deletions

View file

@ -85,8 +85,8 @@ COMMIT
# DUMMY interfaces are the proper way to install IPs without assigning them any
# particular virtual (tun,tap,...) or physical (ethernet) interface.
# Accept DNS traffic to the local DNS resolver
-A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT
# Accept DNS traffic to the local DNS resolver from VPN clients only
-A INPUT -s {{ subnets | join(',') }} -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT
# Drop traffic between VPN clients
-A FORWARD -s {{ subnets | join(',') }} -d {{ subnets | join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }}

View file

@ -95,8 +95,8 @@ COMMIT
# DUMMY interfaces are the proper way to install IPs without assigning them any
# particular virtual (tun,tap,...) or physical (ethernet) interface.
# Accept DNS traffic to the local DNS resolver
-A INPUT -d {{ local_service_ipv6 }}/128 -p udp --dport 53 -j ACCEPT
# Accept DNS traffic to the local DNS resolver from VPN clients only
-A INPUT -s {{ subnets | join(',') }} -d {{ local_service_ipv6 }}/128 -p udp --dport 53 -j ACCEPT
# Drop traffic between VPN clients
-A FORWARD -s {{ subnets | join(',') }} -d {{ subnets | join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }}

View file

@ -212,5 +212,35 @@ def test_output_interface_in_nat_rules():
assert "-A POSTROUTING -s 10.48.0.0/16 -j MASQUERADE" not in result
def test_dns_firewall_restricted_to_vpn():
"""Test that DNS access is restricted to VPN clients only."""
template = load_template("rules.v4.j2")
result = template.render(
ipsec_enabled=True,
wireguard_enabled=True,
strongswan_network="10.48.0.0/16",
wireguard_network_ipv4="10.49.0.0/16",
strongswan_network_ipv6="2001:db8::/48",
wireguard_network_ipv6="2001:db8:a160::/48",
wireguard_port=51820,
wireguard_port_avoid=53,
wireguard_port_actual=51820,
ansible_default_ipv4={"interface": "eth0"},
snat_aipv4=None,
BetweenClients_DROP=True,
block_smb=True,
block_netbios=True,
local_service_ip="172.23.198.242",
ansible_ssh_port=22,
reduce_mtu=0,
)
# DNS should only be accessible from VPN subnets
assert "-A INPUT -s 10.48.0.0/16,10.49.0.0/16 -d 172.23.198.242 -p udp --dport 53 -j ACCEPT" in result
# Should NOT have unrestricted DNS access
assert "-A INPUT -d 172.23.198.242 -p udp --dport 53 -j ACCEPT" not in result
if __name__ == "__main__":
pytest.main([__file__, "-v"])