Files
fips/packaging/common/fips.nft
Johnathan Corgan faecddcad4 Correct what the gateway documentation says the mesh firewall does to port forwards
The gateway how-to said the mesh firewall baseline gates port forwards
and that a fips.d drop-in restricts them. It does neither: the gateway
rewrites a forwarded connection's destination in prerouting, so the
packet takes the forward path, while fips.nft and its drop-ins hook
input only. With the baseline loaded, every mesh node that reaches the
gateway reaches every port forward.

The how-to now says so and gives a restriction that works: a separate
table with a forward-hook chain that matches the post-DNAT target
address and port, scoped to the mesh interface through its own fips_if
define, ending in a drop of other translated connections, loaded
alongside the baseline through a fips-firewall.service drop-in. On
OpenWrt it points at the packaged zone's src fips rule instead. The
mesh firewall how-to and the fips.nft header say port forwards are
outside the baseline, and the how-to's TUN rename section says to
change the restriction's fips_if too. The gateway troubleshooting step
and the host-a-service tutorial no longer describe the input baseline
as gating port forwards.

Also corrected: the gateway design document attributed the virtual-IP
source on replies to the per-mapping SNAT, which only sees the first
packet of a new flow sourced from a mapped mesh address; conntrack
reversing the DNAT is what does it. It also said port forwards are
validated on every config reload, but the gateway has no reload. The
configuration reference said the gateway terminates forwarded
connections; the kernel translates and forwards them.
2026-10-14 10:02:51 +00:00

146 lines
6.5 KiB
Plaintext

#!/usr/sbin/nft -f
#
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ fips0 nftables policy — security baseline ║
# ╚═══════════════════════════════════════════════════════════════════╝
#
# Polices ONLY the FIPS mesh interface named by `fips_if` below —
# every packet not arriving on it returns immediately, leaving Docker,
# Tor, OPNsense, the host's main /etc/nftables.conf, and any other
# firewall undisturbed.
#
# ─── THE MESH INTERFACE NAME ────────────────────────────────────────
#
# `fips_if` must equal the daemon's `tun.name` in fips.yaml (default
# "fips0"). If you rename the TUN, change the define to match and
# reload. Until you do, this table polices an interface that does not
# carry mesh traffic, and every listener on the renamed TUN is
# reachable from the mesh. The daemon logs a warning at startup when
# the loaded table is scoped to another interface, and fipstop's
# listening panel reports every listener as exposed.
#
# ─── DEFAULT POLICY ─────────────────────────────────────────────────
#
# INBOUND on mesh: 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 mesh: unrestricted
#
# Why default-deny: services binding to 0.0.0.0 or [::] accept
# connections on the mesh interface 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.
# Its port forwards are forwarded traffic, which
# this input chain never sees; to restrict them,
# see docs/how-to/deploy-gateway.md.
#
# 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-mesh traffic makes this chain a no-op for anything
# not on the mesh interface, so coexistence is safe.
# The mesh interface this table polices. Must equal `tun.name`.
define fips_if = "fips0"
# ━━━ 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-mesh traffic returns immediately — chain is a no-op
# for anything not arriving on the mesh interface.
iifname != $fips_if 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 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━