From a6567f9cf3834f9e9d0ae42ca93c79785058802a Mon Sep 17 00:00:00 2001 From: Arjen <18398758+Origami74@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:31:45 -0400 Subject: [PATCH] Add NixOS flake module (nixosModules.default) + overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the fips daemon as a managed NixOS service so flake consumers can enable it with a single line instead of hand-rolling a systemd unit. Flake outputs (system-independent, outside eachDefaultSystem): - overlays.default — adds pkgs.fips - nixosModules.default — packaging/nixos/ module providing services.fips.* Module options (services.fips): - enable (bool, default false) — main mesh daemon - package (package, default pkgs.fips via overlay) - configFile (path, default /share/fips/fips.yaml) — seed source - openFirewall (bool, default true) — UDP 2121 + TCP 8443 - dns.enable (bool, default true) — route .fips to [::1]:5354 via systemd-resolved (declarative, no setup/teardown scripts) - gateway.enable(bool, default false) — outbound LAN gateway service Hybrid config pattern: fips.yaml + identity keys live in /var/lib/fips/ (writable, seeded on first run only); hosts/ACL files stay at /etc/fips/ because fips hardcodes those paths on Linux. Launched with --config so fips never loads /etc/fips/fips.yaml by accident. flake.nix: ship fips.yaml, hosts, and fips.nft via postInstall so the module can reference them from /share/fips/ without the source tree. Also fix deprecated stdenv.isLinux -> stdenv.hostPlatform.isLinux and platforms.linux ++ darwin -> platforms.unix. packaging/README.md: document the overlay + module and show a full flake.nix consumer example. --- flake.nix | 32 ++++-- packaging/README.md | 42 ++++++- packaging/nixos/README.md | 142 +++++++++++++++++++++++ packaging/nixos/default.nix | 218 ++++++++++++++++++++++++++++++++++++ 4 files changed, 425 insertions(+), 9 deletions(-) create mode 100644 packaging/nixos/README.md create mode 100644 packaging/nixos/default.nix diff --git a/flake.nix b/flake.nix index b760c32e..03491af7 100644 --- a/flake.nix +++ b/flake.nix @@ -44,7 +44,7 @@ rustPlatform.bindgenHook # sets LIBCLANG_PATH + clang for bindgen ]; - buildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ + buildInputs = pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.dbus # libdbus-1.so.3, linked via bluer→libdbus-sys pkgs.stdenv.cc.cc.lib # libgcc_s.so.1, needed by every Rust binary ]; @@ -57,21 +57,29 @@ src = ./.; # Drop the build dir and the usual editor/VCS noise so the source # hash is stable and unrelated edits don't trigger rebuilds. - filter = - path: type: - (pkgs.lib.cleanSourceFilter path type) && (baseNameOf path != "target"); + filter = path: type: (pkgs.lib.cleanSourceFilter path type) && (baseNameOf path != "target"); }; cargoLock.lockFile = ./Cargo.lock; inherit buildInputs; + # Ship the default config, hosts file, and nftables baseline so + # the NixOS module can reference them via $out/share/fips/ without + # needing the source tree. DNS routing is handled declaratively + # via services.resolved on NixOS (no setup/teardown scripts needed). + postInstall = '' + install -Dm 0644 ${./packaging/common/fips.yaml} $out/share/fips/fips.yaml + install -Dm 0644 ${./packaging/common/hosts} $out/share/fips/hosts + install -Dm 0644 ${./packaging/common/fips.nft} $out/share/fips/fips.nft + ''; + # autoPatchelfHook rewrites the RPATH of the built binaries so the # daemon finds libdbus-1.so.3 (linked via bluer→libdbus-sys) in the # Nix store at runtime — without it the `fips` binary fails to load # on NixOS where there is no global /usr/lib. nativeBuildInputs = - nativeBuildInputs ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.autoPatchelfHook ]; + nativeBuildInputs ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.autoPatchelfHook ]; # The test suite exercises TUN devices, raw sockets and mDNS, none of # which exist in the build sandbox. The AUR/Debian packaging likewise @@ -84,7 +92,7 @@ homepage = cargoToml.package.homepage; license = pkgs.lib.licenses.mit; mainProgram = "fips"; - platforms = pkgs.lib.platforms.linux ++ pkgs.lib.platforms.darwin; + platforms = pkgs.lib.platforms.unix; }; }; @@ -124,5 +132,15 @@ formatter = pkgs.nixfmt; } - ); + ) + // { + # System-independent outputs — outside eachDefaultSystem. + # Overlay so consumers get pkgs.fips automatically. + overlays.default = final: prev: { + fips = self.packages.${final.system}.default; + }; + + # NixOS module — consumers import this and set services.fips.enable = true. + nixosModules.default = import ./packaging/nixos; + }; } diff --git a/packaging/README.md b/packaging/README.md index 0552dd6c..ee9f39f5 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -48,6 +48,7 @@ packaging/ debian/ Debian/Ubuntu .deb packaging via cargo-deb freebsd/ FreeBSD .pkg packaging via pkg-create(8) macos/ macOS .pkg installer via pkgbuild + nixos/ NixOS flake module (services.fips.*) systemd/ Generic Linux systemd tarball packaging openwrt-ipk/ OpenWrt .ipk packaging via cargo-zigbuild (opkg) openwrt-apk/ OpenWrt .apk packaging via cargo-zigbuild + apk mkpkg @@ -244,8 +245,45 @@ nix develop # dev shell with the pinned toolchain + cargo-edit nix flake check # build + validate the flake ``` -Add to a NixOS configuration via the flake's `packages..fips` -output, e.g. `environment.systemPackages = [ fips.packages.${system}.default ];`. +The flake also exposes: + +- `overlays.default` — adds `pkgs.fips` to nixpkgs +- `nixosModules.default` — a NixOS module (`packaging/nixos/`) that provides + `services.fips.enable` and runs the daemon as a systemd service + +**As a package only** (no service management): + +```nix +environment.systemPackages = [ fips.packages.${system}.default ]; +``` + +**As a managed NixOS service** (recommended — starts on boot, journalctl logs): + +```nix +# flake.nix +{ + inputs.fips = { + url = "github:jmcorgan/fips"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, fips, ... }@inputs: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit inputs; }; + modules = [ + ./configuration.nix + fips.nixosModules.default + { services.fips.enable = true; } + ]; + }; + }; +} +``` + +See [`packaging/nixos/README.md`](nixos/README.md) for the full option +reference (`services.fips.enable`, `.package`, `.configFile`, +`.openFirewall`). ## Shared Assets diff --git a/packaging/nixos/README.md b/packaging/nixos/README.md new file mode 100644 index 00000000..9def8d04 --- /dev/null +++ b/packaging/nixos/README.md @@ -0,0 +1,142 @@ +# NixOS Packaging + +NixOS module and flake outputs for FIPS. + +## Quick Start (flake consumers) + +Add fips as a flake input and enable the service: + +```nix +# flake.nix +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + fips = { + url = "github:jmcorgan/fips"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, fips, ... }@inputs: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + specialArgs = { inherit inputs; }; + modules = [ + ./configuration.nix + fips.nixosModules.default # ← import the module + { + services.fips.enable = true; # ← enable the daemon + } + ]; + }; + }; +} +``` + +Apply with: + +```sh +sudo nixos-rebuild switch --flake .#myhost +``` + +## Options + +| Option | Type | Default | Description | +|---|---|---|---| +| `services.fips.enable` | bool | `false` | Enable the FIPS mesh network daemon | +| `services.fips.package` | package | `pkgs.fips` | The fips package (provided via overlay) | +| `services.fips.configFile` | path | shipped default | Default `fips.yaml` used to seed `/var/lib/fips/fips.yaml` on first run | +| `services.fips.openFirewall` | bool | `true` | Open UDP 2121 + TCP 8443 | +| `services.fips.dns.enable` | bool | `true` | Route `.fips` queries to the fips DNS responder via systemd-resolved | +| `services.fips.gateway.enable` | bool | `false` | Enable the outbound LAN gateway (`fips-gateway.service`) | + +## What it does + +- Installs the `fips`, `fipsctl`, `fipstop`, and `fips-gateway` binaries +- Creates a `fips` group (add your user for non-sudo `fipsctl`/`fipstop`) +- Seeds `/var/lib/fips/fips.yaml` and `/etc/fips/hosts` from the shipped + defaults on first run only — operator edits are never clobbered +- Runs `fips` as a systemd service (`fips.service`) as `root:fips` + (root is required for TUN interfaces and raw sockets) +- Routes `.fips` DNS queries to the fips responder on `[::1]:5354` via + systemd-resolved (enabled by default; disable with `services.fips.dns.enable = false`) +- Optionally runs the outbound LAN gateway (`fips-gateway.service`) +- Opens firewall ports for UDP (2121) and TCP (8443) transports + +## Config file layout (hybrid pattern) + +FIPS uses a hybrid config pattern that balances declarative defaults with +operator-editable runtime state: + +| Path | Purpose | Writable | Seeded from | +|---|---|---|---| +| `/var/lib/fips/fips.yaml` | Main config | yes | `services.fips.configFile` (first run only) | +| `/var/lib/fips/fips.key` | Node identity (private) | yes | generated by fips on first start | +| `/var/lib/fips/fips.pub` | Node identity (public) | yes | generated by fips on first start | +| `/etc/fips/hosts` | Static hostname → npub map | yes | shipped `hosts` (first run only) | +| `/etc/fips/peers.allow` | Peer allowlist (ACL) | yes | operator-created | +| `/etc/fips/peers.deny` | Peer denylist (ACL) | yes | operator-created | +| `/etc/fips/fips.d/` | nftables drop-in rules | yes | operator-created | + +### Why two locations? + +- **`/var/lib/fips/`** holds the main config and identity keys. This survives + system rebuilds and reboots. fips derives key paths from the config file's + parent directory, so `fips.key`/`fips.pub` land here automatically. +- **`/etc/fips/`** holds the hosts file and ACL files. fips **hardcodes** these + paths on Linux (`DEFAULT_HOSTS_PATH`, `DEFAULT_PEERS_ALLOW_PATH`, + `DEFAULT_PEERS_DENY_PATH`), so they cannot be relocated. + +### Why not `environment.etc`? + +NixOS `environment.etc` creates symlinks into the read-only Nix store. That +makes files immutable at runtime and would clobber operator edits on every +rebuild. The module instead seeds real writable files via a `preStart` +script that only runs when the target file is absent. + +### Why `--config /var/lib/fips/fips.yaml`? + +fips has a config search path (`./fips.yaml`, `~/.config/fips/fips.yaml`, +`/etc/fips/fips.yaml`). Passing `--config` explicitly bypasses that search +path entirely, so fips loads **only** the user-managed file and never +accidentally picks up a stale `/etc/fips/fips.yaml`. + +## Usage after install + +```sh +sudo journalctl -u fips -f # follow logs +fipsctl show status # check status (needs fips group membership) +fipstop # live monitoring +``` + +To use `fipsctl`/`fipstop` without sudo, add your user to the `fips` group: + +```nix +users.users.myuser.extraGroups = [ "fips" ]; +``` + +Log out and back in for the group change to take effect. + +### Editing config at runtime + +```sh +sudo nano /var/lib/fips/fips.yaml # edit main config +sudo nano /etc/fips/hosts # add static hostname mappings +sudo nano /etc/fips/peers.allow # add peers to the allowlist +sudo systemctl restart fips # apply changes +``` + +Files are group-writable (`0664`, owned by `root:fips`), so `fips` group +members can edit without sudo. + +## Files + +``` +packaging/nixos/ +├── default.nix # NixOS module (services.fips.*) +└── README.md # this file +``` + +The flake also exposes: + +- `overlays.default` — adds `pkgs.fips` +- `nixosModules.default` — the NixOS module diff --git a/packaging/nixos/default.nix b/packaging/nixos/default.nix new file mode 100644 index 00000000..829a6eac --- /dev/null +++ b/packaging/nixos/default.nix @@ -0,0 +1,218 @@ +# NixOS module for the FIPS mesh network daemon. +# +# Exposed as nixosModules.default in flake.nix. +# Consumers enable it with: +# +# { inputs, ... }: { +# imports = [ inputs.fips.nixosModules.default ]; +# services.fips.enable = true; +# } +# +# The package is provided via the flake's overlay (overlays.default), so it +# lands in pkgs.fips without the consumer needing to know the input name. +# +# --- Hybrid config pattern --- +# +# fips.yaml and identity keys live in /var/lib/fips/ (writable, survives +# reboots and rebuilds). The shipped default is seeded there on first run +# only; operator edits are never clobbered. +# +# The hosts file and ACL files (peers.allow, peers.deny) MUST stay at +# /etc/fips/ because fips hardcodes those paths on Linux +# (DEFAULT_HOSTS_PATH, DEFAULT_PEERS_ALLOW_PATH, DEFAULT_PEERS_DENY_PATH). +# They are seeded as real writable files (not Nix store symlinks) so the +# operator can edit them directly. +# +# fips is launched with --config /var/lib/fips/fips.yaml, which bypasses +# the config search path entirely — fips will never accidentally load +# /etc/fips/fips.yaml instead of the user-managed version. +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.fips; + stateDir = "/var/lib/fips"; +in +{ + options.services.fips = { + enable = lib.mkEnableOption "FIPS mesh network daemon"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.fips; + defaultText = lib.literalExpression "pkgs.fips"; + description = '' + The fips package to use. Defaults to the one provided by the + fips flake overlay (overlays.default). + ''; + }; + + configFile = lib.mkOption { + type = lib.types.path; + default = "${cfg.package}/share/fips/fips.yaml"; + defaultText = lib.literalExpression "''${cfg.package}/share/fips/fips.yaml"; + description = '' + Default fips.yaml used to seed the writable config at + /var/lib/fips/fips.yaml on first run. The operator can then edit + /var/lib/fips/fips.yaml directly; this file is never overwritten + after the initial seed. + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Open firewall ports for fips transports + (UDP 2121, TCP 8443). + ''; + }; + + dns = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Route .fips queries to the fips DNS responder on [::1]:5354 + via systemd-resolved. Without this, .fips hostnames don't + resolve on the host. + ''; + }; + }; + + gateway = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable the outbound LAN gateway. Lets non-fips hosts on the + LAN reach mesh destinations via DNS-allocated virtual IPs + and kernel NAT. Requires the fips daemon running with DNS. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + # 'fips' group so non-root users can run fipsctl/fipstop without sudo + # and edit config/hosts files group-writable. + users.groups.fips = { }; + + # Writable directories for hosts file, ACL files, and nftables drop-ins. + # fips hardcodes /etc/fips/hosts, /etc/fips/peers.allow, + # /etc/fips/peers.deny on Linux, so these MUST live here. Created as + # real directories (not environment.etc symlinks) so the operator can + # edit files in place. + systemd.tmpfiles.rules = [ + "d /etc/fips 0775 root fips -" + "d /etc/fips/fips.d 0775 root fips -" + ]; + + systemd.services.fips = { + description = "FIPS Mesh Network Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ + "network-online.target" + "systemd-tmpfiles-setup.service" + ]; + wants = [ "network-online.target" ]; + + # Seed config and hosts on first run only. Existing files are never + # overwritten, so operator edits survive service restarts and system + # rebuilds. + preStart = '' + # Seed writable config from the shipped default (only if absent). + # fips derives key paths (fips.key/fips.pub) from the config file's + # parent directory, so keys also land in /var/lib/fips/. + if [ ! -f ${stateDir}/fips.yaml ]; then + cp "${cfg.configFile}" ${stateDir}/fips.yaml + chown root:fips ${stateDir}/fips.yaml + chmod 0664 ${stateDir}/fips.yaml + fi + + # Seed hosts file (fips hardcodes /etc/fips/hosts on Linux). + if [ ! -f /etc/fips/hosts ]; then + cp "${cfg.package}/share/fips/hosts" /etc/fips/hosts + chown root:fips /etc/fips/hosts + chmod 0664 /etc/fips/hosts + fi + ''; + + serviceConfig = { + Type = "simple"; + # Run as root:fips so the daemon has root for TUN/raw sockets while + # group members can access the control socket and state files. + Group = "fips"; + # --config bypasses the search path: fips loads ONLY this file, + # never /etc/fips/fips.yaml. + ExecStart = "${cfg.package}/bin/fips --config ${stateDir}/fips.yaml"; + Restart = "on-failure"; + RestartSec = 5; + + # Writable state directory (/var/lib/fips/) for config + keys. + # root:fips 0775 so group members can edit the config. + StateDirectory = "fips"; + StateDirectoryMode = "0775"; + + # Control socket directory (/run/fips/) — group-accessible. + RuntimeDirectory = "fips"; + RuntimeDirectoryMode = "0770"; + + # Log directory (/var/log/fips/) for the built-in profiler. + LogsDirectory = "fips"; + + # Security hardening (daemon runs as root for TUN and raw sockets). + # Mirrors packaging/systemd/fips.service. + ProtectHome = "yes"; + PrivateTmp = true; + ProtectKernelModules = true; + ProtectKernelTunables = false; + }; + }; + + services.resolved = lib.mkIf cfg.dns.enable { + enable = true; + settings.Resolve = { + DNS = [ "[::1]:5354" ]; + Domains = [ "~fips" ]; + }; + }; + + systemd.services.fips-gateway = lib.mkIf cfg.gateway.enable { + description = "FIPS Outbound LAN Gateway"; + wantedBy = [ "multi-user.target" ]; + after = [ "fips.service" ]; + requires = [ "fips.service" ]; + + preStart = '' + for i in $(seq 1 30); do + ${pkgs.iproute2}/bin/ip link show fips0 >/dev/null 2>&1 && exit 0 + sleep 1 + done + echo "fips0 did not appear within 30s" >&2 + exit 1 + ''; + + serviceConfig = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/fips-gateway --config ${stateDir}/fips.yaml"; + Restart = "on-failure"; + RestartSec = 5; + TimeoutStopSec = 15; + ProtectHome = "yes"; + PrivateTmp = true; + ProtectKernelModules = true; + ProtectKernelTunables = false; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ 8443 ]; + allowedUDPPorts = [ 2121 ]; + }; + }; +}