From dbe8f23cddf66be2e5f190b203ead4c32b1b737f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 17 Aug 2025 19:19:00 -0400 Subject: [PATCH] Restrict DNS access to VPN clients only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- roles/common/templates/rules.v4.j2 | 4 ++-- roles/common/templates/rules.v6.j2 | 4 ++-- tests/unit/test_iptables_rules.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/roles/common/templates/rules.v4.j2 b/roles/common/templates/rules.v4.j2 index 4e1ba86f..9ed8a502 100644 --- a/roles/common/templates/rules.v4.j2 +++ b/roles/common/templates/rules.v4.j2 @@ -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" }} diff --git a/roles/common/templates/rules.v6.j2 b/roles/common/templates/rules.v6.j2 index 8d90beb1..e060b513 100644 --- a/roles/common/templates/rules.v6.j2 +++ b/roles/common/templates/rules.v6.j2 @@ -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" }} diff --git a/tests/unit/test_iptables_rules.py b/tests/unit/test_iptables_rules.py index ec561b5a..1d459db9 100644 --- a/tests/unit/test_iptables_rules.py +++ b/tests/unit/test_iptables_rules.py @@ -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"])