mirror of
https://github.com/jmcorgan/fips.git
synced 2026-09-14 00:45:08 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user