Date: 2026-05-30
Accepted
Islandr must enforce ACLs at the packet level on the hub VM: for each enabled peer, the set of target networks the peer's groups allow, and a default-deny for everything else.
The hub VM currently uses ufw (Uncomplicated Firewall) for hand-written rules. ufw is a thin wrapper around iptables (or nftables underneath, depending on distro version) optimized for human-typed commands. It is not optimized for programmatic management.
Islandr's enforcement loop is: ACL changes → recompute ruleset → validate → atomically reload. That loop runs on every peer create/disable/delete and every group/network change. Failure modes that matter:
/etc/ufw/user.rules) that drifts from the in-kernel state because Islandr writes one but ufw rewrites the other.We need a firewall layer whose model maps cleanly onto: "atomically replace this table with this content; validate first".
Use nftables directly via the nft CLI. Drop ufw on the hub VM entirely.
inet islandr. It never touches other tables.nft -c -f <tempfile>, and on success replaces the live table with nft -f <tempfile>. nftables guarantees atomic table replacement.nft stderr, keep the existing live table, surface the error in the UI.(peer.assignedIP, resource.ip, transport, port) tuple — one accept rule per tuple. Site CIDRs are not used as nftables targets; they appear only in peer .conf AllowedIPs for routing. (ADR-0006 defines this granularity and supersedes the named-set approach considered during initial design.) This keeps the ruleset explicit, auditable, and easy to verify with nft list ruleset.systemctl mask ufw) so an admin reflex of ufw allow 22 doesn't conflict with the Islandr-owned table.islandr table. They live in a separate inet filter input table maintained out-of-band by the operator, so an Islandr bug cannot lock the operator out.Example generated ruleset (illustrative — peer 10.8.0.5 has RDP+SSH to Terminal-01; peer 10.8.0.6 has RDP only):
table inet islandr {
chain forward {
type filter hook forward priority 0; policy drop;
iifname "wg0" ip saddr 10.8.0.5 ip daddr 10.20.0.5 tcp dport 3389 accept
iifname "wg0" ip saddr 10.8.0.5 ip daddr 10.20.0.5 tcp dport 22 accept
iifname "wg0" ip saddr 10.8.0.6 ip daddr 10.20.0.5 tcp dport 3389 accept
}
}
Baseline: nftables direct (the decision).
| Criterion (weight) | nftables direct (baseline) | ufw (status quo) | iptables direct | firewalld |
|---|---|---|---|---|
| Atomic ruleset replacement (5) | 0 | -1 | -1 | 0 |
Validation before apply (-c) (4) |
0 | -1 | -1 | 0 |
| Programmatic management ergonomics (4) | 0 | -1 | -1 | 0 |
| Named sets for ACL targets (3) | 0 | -1 | -1 | -1 |
| State file vs. in-kernel drift risk (3) | 0 | -1 | 0 | -1 |
| Modern / future-proof (2) | 0 | 0 | -1 | 0 |
| Operator familiarity (2) | 0 | +1 | 0 | -1 |
| Weighted total | 0 | −16 | −15 | −9 |
Notes:
iptables-restore does support atomic-ish replace, but the rule format is less expressive (no named interval sets for CIDRs), and iptables is on its way out.The matrix is decisive. The only column where ufw wins is operator familiarity, which is also the column that creates the trap: an operator who reflexively types ufw allow adds rules outside Islandr's model and creates exactly the drift this ADR is trying to prevent.
Positive
nft -c -f → nft -f; either both rules and sets update together or neither does.SELECT … FROM peers JOIN users JOIN groups JOIN networks.Risks created
-c) catches syntax, not intent. Mitigation: integration test the generator against known peer/group/network fixtures; smoke test in CI that golden-file rulesets parse and behave; an "preview ruleset" view in the admin UI before apply.islandr table means there are now two firewall surfaces on the hub VM. An operator could mis-edit the input table and break SSH. Mitigation: ship a documented "minimal input table" template alongside Islandr; deployment guide spells out the boundary.Accepted trade-offs
-c check mode