mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Add a default-deny nftables ruleset for the fips0 mesh interface as a packaged operator asset, with a companion fips-firewall.service oneshot unit for systemd hosts. Both are shipped disabled — the baseline is an operator conffile and the unit is intentionally not enabled in postinst. Activation is an explicit one-liner: sudo systemctl enable --now fips-firewall.service This is deliberate: silently mutating host firewall state on package install is hostile across the axes that matter (collisions with existing operator nftables / Docker / OPNsense rulesets, surprise behaviour for hosts that already filter elsewhere, conversion of an explicit security decision into an invisible one). The opt-in posture preserves operator agency. The baseline closes a real default-exposure gap: any service on a mesh host bound to a wildcard address (0.0.0.0 or [::]) is reachable from every authenticated peer in the mesh by default. Identity on the mesh is the peer's npub but identity is not authorization, and the mesh is closer to a shared LAN than to the public internet. With this filter loaded, the surface is closed unless a drop-in opens it explicitly. Baseline shape: - Early-return for non-fips0 traffic (every other firewall left undisturbed) - conntrack established/related accept (replies to outbound flows) - ICMPv6 echo-request accept (ping6 reachability) - include "/etc/fips/fips.d/*.nft" — operator-supplied allowances - counter drop default The accompanying docs/fips-security.md lays out the threat model (npub-authenticated mesh is closer to a shared LAN than to the public internet — identity is not authorization), the activation workflow, drop-in extension recipes (allow inbound SSH from a specific peer fd97:.../128, allow HTTP from one /64, etc), drop visibility / debugging via the journal log rule and the drop counter, coexistence with the runtime-managed `inet fips_gateway` table, what the baseline does NOT cover (outbound, application auth, mesh handshake ACL = PR #50 / IDEA-0047 territory), and future cross-OS work (macOS PF baseline, OpenWrt fw4, gateway abstraction). Packaging: - packaging/common/fips.nft → /etc/fips/fips.nft (conffile) - packaging/debian/fips-firewall.service → /lib/systemd/system/ - docs/fips-security.md → /usr/share/doc/fips/ - postinst creates /etc/fips/fips.d/ (mode 0755) on configure - prerm stops/disables fips-firewall.service on remove/purge OpenWrt fw4 path and macOS PF baseline are deferred — separate asymmetries, separate work.
129 lines
5.6 KiB
Plaintext
129 lines
5.6 KiB
Plaintext
#!/usr/sbin/nft -f
|
|
#
|
|
# ╔═══════════════════════════════════════════════════════════════════╗
|
|
# ║ fips0 nftables policy — security baseline ║
|
|
# ╚═══════════════════════════════════════════════════════════════════╝
|
|
#
|
|
# Polices ONLY the fips0 mesh interface — every packet not arriving
|
|
# on fips0 returns immediately, leaving Docker, Tor, OPNsense, the
|
|
# host's main /etc/nftables.conf, and any other firewall undisturbed.
|
|
#
|
|
# ─── DEFAULT POLICY ─────────────────────────────────────────────────
|
|
#
|
|
# INBOUND on fips0: DROP everything except —
|
|
# • replies to outbound flows we initiated
|
|
# (conntrack established/related)
|
|
# • ICMPv6 echo-request (ping6 reachability)
|
|
# • rules in /etc/fips/fips.d/*.nft drop-ins
|
|
#
|
|
# OUTBOUND from fips0: unrestricted
|
|
#
|
|
# Why default-deny: services binding to 0.0.0.0 or [::] accept
|
|
# connections on fips0 by default. This baseline ensures that
|
|
# accidental exposure of HTTP, SSH, databases, etc. through the
|
|
# mesh is impossible without explicit operator action.
|
|
#
|
|
# ─── LOADING / RELOADING ────────────────────────────────────────────
|
|
#
|
|
# Apply this file:
|
|
#
|
|
# sudo nft -f /etc/fips/fips.nft
|
|
#
|
|
# To make it persist across reboots, load it from your preferred
|
|
# mechanism: a systemd unit that runs `nft -f` on start, an include
|
|
# from /etc/nftables.conf, etc.
|
|
#
|
|
# Verify the loaded ruleset:
|
|
#
|
|
# sudo nft list table inet fips
|
|
#
|
|
# ─── ADDING INBOUND ALLOWANCES ──────────────────────────────────────
|
|
#
|
|
# Drop a file into /etc/fips/fips.d/ ending in .nft. Each file is
|
|
# included inline into the `inbound` chain at the marked point below.
|
|
# Format: chain-context rule lines, one per line. For example:
|
|
#
|
|
# /etc/fips/fips.d/services.nft
|
|
# ────────────────────────────
|
|
# tcp dport 22 accept
|
|
# tcp dport 80 accept
|
|
# udp dport 53 accept
|
|
# ip6 saddr fd97:467a::/64 tcp dport 22 accept # SSH from one /64
|
|
# ip6 saddr fd97:.../128 tcp dport 8443 accept # one specific peer
|
|
#
|
|
# After editing, reload:
|
|
#
|
|
# sudo nft -f /etc/fips/fips.nft
|
|
#
|
|
# (or trigger your loading mechanism's reload).
|
|
#
|
|
# Source-based filtering uses the peer's mesh address (npub-derived
|
|
# fd97:... format). To find a peer's mesh address, look in their
|
|
# fips.pub or query the daemon.
|
|
#
|
|
# Note: an empty /etc/fips/fips.d/ is fine — the include glob simply
|
|
# matches nothing and the baseline policy applies as-is.
|
|
#
|
|
# ─── DEBUGGING / DROP VISIBILITY ────────────────────────────────────
|
|
#
|
|
# To see drops in the systemd journal:
|
|
# 1. Uncomment the `log` rule near the bottom of the inbound chain.
|
|
# 2. Reload: sudo nft -f /etc/fips/fips.nft
|
|
# 3. Tail: sudo journalctl -k -f -g "fips drop:"
|
|
#
|
|
# Drop counter (always on, no log):
|
|
# sudo nft list table inet fips
|
|
#
|
|
# ─── COEXISTENCE WITH OTHER NFTABLES TABLES ─────────────────────────
|
|
#
|
|
# This table lives in the `inet` family. Other tables you may see:
|
|
#
|
|
# inet fips_gateway — created by the fips-gateway binary when
|
|
# running (separate concern from this file).
|
|
# See the fips-gateway design doc for details.
|
|
#
|
|
# ip / ip6 family — Docker, /etc/nftables.conf, etc.
|
|
# Independent of this table.
|
|
#
|
|
# All hook at standard filter priority (0). Order between same-family
|
|
# same-priority tables is determined by load order, but the early-
|
|
# return for non-fips0 traffic makes this chain a no-op for anything
|
|
# not on fips0, so coexistence is safe.
|
|
|
|
# ━━━ Idempotent table replacement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
add table inet fips
|
|
flush table inet fips
|
|
|
|
table inet fips {
|
|
|
|
chain inbound {
|
|
type filter hook input priority 0; policy accept;
|
|
|
|
# Non-fips0 traffic returns immediately — chain is a no-op
|
|
# for anything not arriving on the fips0 mesh interface.
|
|
iifname != "fips0" return
|
|
|
|
# Replies to outbound flows + related ICMPv6 errors
|
|
# (Packet Too Big for PMTU, Destination Unreachable, etc.)
|
|
ct state established,related accept
|
|
|
|
# ICMPv6 echo-request — ping6 reachability tests.
|
|
meta nfproto ipv6 icmpv6 type echo-request accept
|
|
|
|
# Operator-supplied allow rules from drop-in files.
|
|
# See "ADDING INBOUND ALLOWANCES" in the header.
|
|
include "/etc/fips/fips.d/*.nft"
|
|
|
|
# Optional: log drops to journal (rate-limited).
|
|
# Uncomment to enable, then reload via `nft -f`.
|
|
#
|
|
#log prefix "fips drop: " level info limit rate 10/minute
|
|
|
|
# Default deny on fips0 inbound. Counter visible via
|
|
# `nft list table inet fips`.
|
|
counter drop
|
|
}
|
|
}
|
|
|
|
# ━━━ END OF FILE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|