mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 07:37:36 +00:00
The packaged mesh firewall had one chain, on the input hook. A fips-gateway port forward is a DNAT in prerouting, so its traffic is routed through the forward hook and never met that chain or its fips.d drop-ins. With the firewall loaded, every mesh node that could reach the gateway reached every forward, and the only way to restrict one was a separate table loaded by hand, which no test exercised. fips.nft now has a second chain in the inet fips table, on the forward hook. It returns for traffic not arriving on the mesh interface, accepts established and related packets, includes drop-ins from /etc/fips/fips.d/forward/*.nft, and then accepts the remaining port-forwarded connections with a counter. Forwards stay open to the mesh by default, so nothing changes on upgrade; a forward is restricted by a drop-in that accepts the allowed mesh addresses for its target address and port and drops the rest. A missing forward/ directory is fine. The static gateway suite gains a phase that loads the tree's fips.nft on the gateway with the drop-in taken from the deployment how-to, and checks that the allowed node reaches the TCP and UDP forwards, the other node is refused on both with each refusal counted by its own drop rule, and a forward with no drop-in line stays open. A packaging test checks the chain's rules and their order on every target. The how-to, the firewall guides, the troubleshooting step, the design and reference documents and the hosting tutorial describe the new chain and drop-in directory in place of the hand-loaded table, and say what happens to a hand-loaded table or a kept, edited fips.nft.
237 lines
8.6 KiB
Markdown
237 lines
8.6 KiB
Markdown
# Enable the Mesh-Interface Firewall
|
|
|
|
FIPS ships a default-deny nftables baseline at `/etc/fips/fips.nft` that
|
|
restricts inbound traffic on the `fips0` mesh interface to conntrack
|
|
replies and ICMPv6 echo. The baseline is **not** enabled by default — see
|
|
[../design/fips-security.md](../design/fips-security.md) for the threat
|
|
model and the rationale behind keeping activation explicit. This guide
|
|
covers the operator steps to load the baseline, extend it with per-host
|
|
allowances, and inspect drops.
|
|
|
|
## Activate the baseline
|
|
|
|
The package ships `fips-firewall.service`, a systemd oneshot that runs
|
|
`nft -f /etc/fips/fips.nft` on start and removes the `inet fips` table
|
|
on stop. To activate:
|
|
|
|
```sh
|
|
sudo systemctl enable --now fips-firewall.service
|
|
```
|
|
|
|
This loads the table now and arranges for it to load on every subsequent
|
|
boot. To disable and tear it down:
|
|
|
|
```sh
|
|
sudo systemctl disable --now fips-firewall.service
|
|
```
|
|
|
|
To reload after editing `/etc/fips/fips.nft` or adding a drop-in under
|
|
`/etc/fips/fips.d/`:
|
|
|
|
```sh
|
|
sudo systemctl reload-or-restart fips-firewall.service
|
|
```
|
|
|
|
The file is idempotent — it begins with `add table inet fips; flush
|
|
table inet fips;` so re-running it replaces the live ruleset atomically.
|
|
Equivalently:
|
|
|
|
```sh
|
|
sudo nft -f /etc/fips/fips.nft
|
|
```
|
|
|
|
## If you rename the TUN
|
|
|
|
The baseline polices one interface, named by the `define fips_if` line
|
|
near the top of `/etc/fips/fips.nft`:
|
|
|
|
```nft
|
|
define fips_if = "fips0"
|
|
```
|
|
|
|
Traffic arriving on any other interface returns from the chain
|
|
unfiltered. The shipped value matches the default `tun.name`. If you set
|
|
`tun.name` in `fips.yaml` to another name, change `fips_if` to the same
|
|
name and reload:
|
|
|
|
```sh
|
|
sudo systemctl reload-or-restart fips-firewall.service
|
|
```
|
|
|
|
Until you do, the baseline filters nothing on the TUN, every listener
|
|
on it is reachable from the mesh, and the `forward` chain returns every
|
|
port-forwarded connection unrestricted. The daemon checks this when it brings
|
|
the TUN up and logs a warning naming both interfaces, and `fipstop`'s
|
|
listening panel shows the "all listeners exposed" banner.
|
|
|
|
Because `/etc/fips/fips.nft` is a configuration file, an upgrade that
|
|
changes it while you have edited it asks whether to keep your version
|
|
(dpkg) or installs the new one beside yours (`.pacnew` on Arch). The
|
|
systemd tarball's `install.sh` never replaces an existing file and
|
|
writes the new one as `/etc/fips/fips.nft.template`. Keep your file and
|
|
carry over any change you want from the new one.
|
|
|
|
## Gateway port forwards
|
|
|
|
The baseline's `inbound` chain polices traffic addressed to this host
|
|
(the `input` hook). A `fips-gateway` port forward rewrites the
|
|
destination to a LAN host, so its traffic is forwarded and never
|
|
reaches that chain or the `fips.d/*.nft` drop-ins. The baseline's
|
|
second chain, `forward`, sees it instead. It accepts port-forwarded
|
|
connections from the mesh unless a drop-in under
|
|
`/etc/fips/fips.d/forward/` refuses them, so with no such drop-in every
|
|
port forward is open to the mesh. To restrict one, see [Compose with
|
|
the mesh firewall](deploy-gateway.md#compose-with-the-mesh-firewall).
|
|
|
|
## Folding the baseline into the host's main nftables
|
|
|
|
If you prefer to load the baseline from your existing
|
|
`/etc/nftables.conf` rather than via the systemd unit, include it
|
|
directly:
|
|
|
|
```nft
|
|
# in /etc/nftables.conf
|
|
include "/etc/fips/fips.nft"
|
|
```
|
|
|
|
In that case do **not** enable `fips-firewall.service` — the host's main
|
|
nftables setup owns the loading. The two paths are mutually exclusive.
|
|
|
|
## Extend with per-host allowances via drop-ins
|
|
|
|
The baseline drops everything inbound on `fips0` except conntrack
|
|
replies and ICMPv6 echo. To open specific services to specific mesh
|
|
nodes, drop a file into `/etc/fips/fips.d/` ending in `.nft`. Each
|
|
file is included inline into the `inbound` chain at the marked point
|
|
and may contain any nftables rule lines valid in that context.
|
|
|
|
Reload after editing:
|
|
|
|
```sh
|
|
sudo systemctl reload-or-restart fips-firewall.service
|
|
# or: sudo nft -f /etc/fips/fips.nft
|
|
```
|
|
|
|
### Allow inbound SSH from a specific mesh node
|
|
|
|
```nft
|
|
# /etc/fips/fips.d/ssh-from-bastion.nft
|
|
ip6 saddr fd97:1234:5678:9abc:def0:1234:5678:9abc tcp dport 22 accept
|
|
```
|
|
|
|
The source filter is the node's mesh address. To find a node's mesh
|
|
address, look in their `fips.pub` (which contains the npub) and derive
|
|
the address from it (only the leading `fd` byte is fixed; the other 15
|
|
come from the node's key), or query the running daemon:
|
|
|
|
```sh
|
|
fipsctl show identity-cache
|
|
fipsctl show peers
|
|
```
|
|
|
|
### Allow inbound DNS broadly
|
|
|
|
Some services need to be reachable from any mesh node (a public DNS
|
|
resolver, a public bootstrap node):
|
|
|
|
```nft
|
|
# /etc/fips/fips.d/dns-public.nft
|
|
udp dport 53 accept
|
|
tcp dport 53 accept
|
|
```
|
|
|
|
Omit the source filter only when the service is intended to be
|
|
universally reachable on the mesh. The baseline's purpose is to make
|
|
"universally reachable" an explicit decision rather than the default.
|
|
|
|
### Multiple nodes, one service
|
|
|
|
```nft
|
|
# /etc/fips/fips.d/git-from-trusted.nft
|
|
ip6 saddr {
|
|
fd97:1111:2222:3333:4444:5555:6666:7777,
|
|
fd97:8888:9999:aaaa:bbbb:cccc:dddd:eeee
|
|
} tcp dport 9418 accept
|
|
```
|
|
|
|
Set syntax keeps multi-node rules readable and is more efficient than a
|
|
chain of individual rules.
|
|
|
|
## Verify with fipstop
|
|
|
|
`fipstop`'s Node tab carries a **Listening on fips0** panel
|
|
(right-half of the Traffic block) that pairs each local IPv6
|
|
listener with its current baseline-filter classification. After
|
|
adding or editing a drop-in and reloading, this is the fastest
|
|
way to confirm the rule landed correctly without manually
|
|
parsing `nft list table inet fips`.
|
|
|
|
| Panel state | Reading |
|
|
| ----------- | ------- |
|
|
| Service row in **default White** with `OPEN` in the State column | The chain has a canonical, unrestricted accept rule for this (proto, port). The service is reachable from any mesh node. |
|
|
| Service row in **DarkGray** with `filt` | No matching accept rule; the chain falls through to `counter drop`. The service is not reachable from the mesh. |
|
|
| Service row in **DarkGray** with `filt?` | A rule references the port but uses matchers the panel cannot fully decompose (saddr filter, jump, daddr filter). The intent is operator-defined; inspect with `sudo nft list table inet fips` to see the actual rule. |
|
|
| **Yellow banner** above the panel: "fips-firewall.service inactive — all listeners exposed" | The `inet fips` table is not loaded, or it is loaded but does not police the TUN (its `fips_if` names another interface, or its scoping rule has a shape the panel cannot read; see [If you rename the TUN](#if-you-rename-the-tun)). Every listener is mesh-reachable (subject only to whatever ACL you have at the peer layer). |
|
|
|
|
A common workflow when extending the baseline is to keep `fipstop`
|
|
open on the Node tab in one terminal while editing
|
|
`/etc/fips/fips.d/` in another. After each
|
|
`sudo systemctl reload-or-restart fips-firewall.service`, the panel
|
|
re-classifies on the next poll tick and the affected row's State
|
|
column flips. A row staying `filt` after you expected `OPEN`
|
|
usually means the drop-in failed to load (syntax error in any file
|
|
under `/etc/fips/fips.d/` aborts the whole reload) or carries a
|
|
saddr filter that triggers `filt?` rather than `OPEN`.
|
|
|
|
The classifier is conservative: it recognizes only the canonical
|
|
unrestricted shapes (`tcp dport N accept`, `udp dport N accept`,
|
|
`dport { ... } accept`, `dport A-B accept`). Source-restricted
|
|
accepts intentionally render as `filt?` rather than `OPEN` —
|
|
the panel is a security screen, and any rule that varies by
|
|
source is an operator decision the panel will not silently bless
|
|
as fully open.
|
|
|
|
## Inspect drops
|
|
|
|
The baseline counter increments on every dropped packet. Inspect it:
|
|
|
|
```sh
|
|
sudo nft list table inet fips
|
|
```
|
|
|
|
Look for the `counter packets N bytes M drop` line at the bottom of the
|
|
`inbound` chain. A non-zero counter means mesh nodes are sending
|
|
traffic that hits the default-deny — usually benign (probes, neighbor
|
|
discovery) but occasionally a misconfigured drop-in.
|
|
|
|
To see which packets are being dropped, uncomment the `log` line near
|
|
the bottom of `/etc/fips/fips.nft`:
|
|
|
|
```nft
|
|
log prefix "fips drop: " level info limit rate 10/minute
|
|
```
|
|
|
|
Reload:
|
|
|
|
```sh
|
|
sudo nft -f /etc/fips/fips.nft
|
|
```
|
|
|
|
Then tail the kernel log:
|
|
|
|
```sh
|
|
sudo journalctl -k -f -g "fips drop:"
|
|
```
|
|
|
|
The rate-limit prevents flooding the journal under sustained probing.
|
|
Adjust the rate, log level, or prefix as needed for the situation.
|
|
Re-comment the rule when you are done; production hosts do not need
|
|
the log line on by default.
|
|
|
|
## See also
|
|
|
|
- [../design/fips-security.md](../design/fips-security.md) — threat
|
|
model, baseline design, and coexistence with other firewalls
|
|
- [../reference/security.md](../reference/security.md) — consolidated
|
|
security reference
|