Installing /etc/fips/fips.yaml as a live dpkg conf-file collides with a
configuration-management-rendered or operator-edited config on upgrade:
dpkg either prompts interactively (keep/replace), stalling unattended
upgrades, or clobbers the local file. Ship the default config as
/usr/share/doc/fips/fips.yaml.example (mode 644) and drop it from
conf-files. postinst now seeds /etc/fips/fips.yaml from the example only
when it does not already exist (mode 600), yielding to any existing
config without a prompt or clobber. Add ConditionPathExists for the
config to the service unit so a missing config skips the unit cleanly
rather than crash-looping.
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.
Replace the resolvectl-only fips-dns.service with a detection script
that configures whichever DNS resolver is available:
1. systemd dns-delegate (systemd >= 258, declarative drop-in)
2. systemd-resolved via resolvectl (most systemd distros)
3. dnsmasq (standalone)
4. NetworkManager with dnsmasq plugin
5. Warning with manual instructions if none found
Service reloads are non-fatal — config is written and the backend
is recorded even if the reload fails, preventing state file cleanup
issues under set -e.
Teardown reads the recorded backend from /run/fips/dns-backend and
reverses the configuration, or cleans up all possible backends if
the state file is missing.
Includes a Docker-based test harness (testing/dns-resolver/test.sh)
covering all five backends across Debian 12, Debian 13, Fedora,
and bare systems.
Fixes#52.
The fips-dns.service unit had three issues:
1. Wants=systemd-resolved.service caused systemd to start systemd-resolved
on systems that weren't using it, breaking existing DNS by rewriting
/etc/resolv.conf to the stub resolver at 127.0.0.53.
2. The ExecStart busy-wait loop for fips0 had no timeout, hanging forever
if fips.service failed to create the TUN device.
3. Installers unconditionally enabled fips-dns.service regardless of whether
systemd-resolved was present.
Fix by replacing Wants= with ConditionPathExists=/run/systemd/resolve (skips
cleanly if resolved isn't running), adding Requires=fips.service (won't start
without the daemon), bounding the fips0 wait loop to 30 seconds, and making
the installers conditional on systemd-resolved being active.
Debian packaging via cargo-deb:
- Add [package.metadata.deb] to Cargo.toml with assets, dependencies,
and conffiles declarations
- Maintainer scripts: postinst (create fips group, enable services,
restart on upgrade), prerm (stop/disable on remove, stop-only on
upgrade), postrm (purge config, keys, group)
- Systemd units (fips.service, fips-dns.service) with /usr/bin/ paths
for .deb installs
- tmpfiles.d entry for /run/fips/ runtime directory
- build-deb.sh wrapper script for building packages
README rewrite:
- Replace run-from-source Quick Start with Building section
- Add Installation options: Debian .deb (cargo-deb) and generic Linux
(systemd tarball)
- Add full Configuration reference with default fips.yaml
- Add Usage sections: DNS resolution, monitoring (fipsctl + fipstop),
service management, and testing
- Update project structure and features list