security: replace WireGuard catch-all ACCEPT with DROP
The PostUp rule appended `iptables -A FORWARD -i wg0 -j ACCEPT` which allowed any WireGuard-connected client full internet access regardless of per-peer rules, even when no peers were configured in wg0.conf. Fix: change PostUp/PostDown to use DROP as the catch-all. Per-peer and per-cell rules use -I (insert at top) so they take precedence; unknown or unconfigured WG traffic hits the DROP at the bottom. Also add reconcile_stale_peer_rules() called on startup to remove FORWARD rules for peer IPs that no longer exist in the registry, preventing deleted peers from retaining firewall access across container restarts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -715,5 +715,84 @@ class TestEnsureCellApiDnat(unittest.TestCase):
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reconcile_stale_peer_rules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReconcileStale(unittest.TestCase):
|
||||
|
||||
def _save_result(self, stdout_text):
|
||||
r = MagicMock()
|
||||
r.returncode = 0
|
||||
r.stdout = stdout_text
|
||||
return r
|
||||
|
||||
def test_returns_zero_when_no_rules(self):
|
||||
with patch.object(firewall_manager, '_wg_exec', return_value=self._save_result('*filter\nCOMMIT\n')):
|
||||
n = firewall_manager.reconcile_stale_peer_rules([])
|
||||
self.assertEqual(n, 0)
|
||||
|
||||
def test_returns_zero_when_all_peers_known(self):
|
||||
save_out = (
|
||||
'*filter\n'
|
||||
'-A FORWARD -s 10.0.0.2 -m comment --comment "pic-peer-10-0-0-2/32" -j ACCEPT\n'
|
||||
'COMMIT\n'
|
||||
)
|
||||
peers = [{'ip': '10.0.0.2'}]
|
||||
with patch.object(firewall_manager, '_wg_exec', return_value=self._save_result(save_out)):
|
||||
n = firewall_manager.reconcile_stale_peer_rules(peers)
|
||||
self.assertEqual(n, 0)
|
||||
|
||||
def test_clears_stale_peer(self):
|
||||
save_out = (
|
||||
'*filter\n'
|
||||
'-A FORWARD -s 10.0.0.9 -m comment --comment "pic-peer-10-0-0-9/32" -j ACCEPT\n'
|
||||
'COMMIT\n'
|
||||
)
|
||||
cleared = []
|
||||
with patch.object(firewall_manager, '_wg_exec', return_value=self._save_result(save_out)):
|
||||
with patch.object(firewall_manager, 'clear_peer_rules', side_effect=cleared.append) as mock_clear:
|
||||
n = firewall_manager.reconcile_stale_peer_rules([])
|
||||
self.assertEqual(n, 1)
|
||||
mock_clear.assert_called_once_with('10.0.0.9')
|
||||
|
||||
def test_handles_cidr_peer_ip(self):
|
||||
"""Peer IPs stored as 'x.x.x.x/32' should still match."""
|
||||
save_out = (
|
||||
'*filter\n'
|
||||
'-A FORWARD -s 10.0.0.5 -m comment --comment "pic-peer-10-0-0-5/32" -j ACCEPT\n'
|
||||
'COMMIT\n'
|
||||
)
|
||||
peers = [{'ip': '10.0.0.5/32'}]
|
||||
with patch.object(firewall_manager, '_wg_exec', return_value=self._save_result(save_out)):
|
||||
with patch.object(firewall_manager, 'clear_peer_rules') as mock_clear:
|
||||
n = firewall_manager.reconcile_stale_peer_rules(peers)
|
||||
self.assertEqual(n, 0)
|
||||
mock_clear.assert_not_called()
|
||||
|
||||
def test_returns_zero_on_iptables_save_failure(self):
|
||||
fail_r = MagicMock()
|
||||
fail_r.returncode = 1
|
||||
fail_r.stdout = ''
|
||||
with patch.object(firewall_manager, '_wg_exec', return_value=fail_r):
|
||||
n = firewall_manager.reconcile_stale_peer_rules([])
|
||||
self.assertEqual(n, 0)
|
||||
|
||||
def test_multiple_stale_ips_all_cleared(self):
|
||||
save_out = (
|
||||
'*filter\n'
|
||||
'-A FORWARD -s 10.0.0.7 -m comment --comment "pic-peer-10-0-0-7/32" -j DROP\n'
|
||||
'-A FORWARD -s 10.0.0.8 -m comment --comment "pic-peer-10-0-0-8/32" -j ACCEPT\n'
|
||||
'COMMIT\n'
|
||||
)
|
||||
cleared = []
|
||||
with patch.object(firewall_manager, '_wg_exec', return_value=self._save_result(save_out)):
|
||||
with patch.object(firewall_manager, 'clear_peer_rules', side_effect=cleared.append):
|
||||
n = firewall_manager.reconcile_stale_peer_rules([])
|
||||
self.assertEqual(n, 2)
|
||||
self.assertIn('10.0.0.7', cleared)
|
||||
self.assertIn('10.0.0.8', cleared)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user