Add NixOS flake module (nixosModules.default) + overlay

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.
This commit is contained in:
Arjen
2026-08-23 17:39:22 +01:00
committed by Johnathan Corgan
parent 6eff0accce
commit a6567f9cf3
4 changed files with 425 additions and 9 deletions
+25 -7
View File
@@ -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;
};
}
+40 -2
View File
@@ -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.<system>.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
+142
View File
@@ -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
+218
View File
@@ -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 ];
};
};
}