#!/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 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
