Security
The framework wires the cross-cutting security plumbing. The trust anchors and the host stay yours. Know the split.
What the framework does
- Secrets off the store.
runtimeSecretsare generated withopensslinto/var/lib/homelab-secrets(root-owned, tight modes), never in the world-readable Nix store. Generation runs sandboxed (ProtectSystem = strict, write scoped to the secrets dir). Each template renders in its own unit, onto tmpfs unless you point itspathsomewhere else. - Durable secret lifecycle.
regenerateIfMissinghandles disposable random secrets.generateOncenames the data path a key protects, and a key lost while that data survives is left absent and logged, never silently replaced. - The edge is the only public surface for HTTP. Services bind
127.0.0.1, and the reverse proxy fronts them. The non-HTTP exceptions each carry their own opt-in and are visible as such: the WireGuard tunnel, Gitea’s SSH server, and the SMB server. Every socket the framework binds is registered, and one assertion checks the whole set for collisions. Each service declaresaccess.model, and the framework gates it accordingly: a per-service OIDC client (group-scoped), orforwardAuth, whose middleware sets the identity headers from the auth response. A service that authenticates nobody itself is not routed at all until a forward-auth provider exists, so the failure mode is an unreachable service rather than an unguarded one. - Hardened service units. Every unit the framework defines runs with
NoNewPrivileges,PrivateTmp,ProtectHomeand the kernel/cgroup protections. A filesystem sandbox (ProtectSystem = strict) is added only where the unit runs as root and writes nothing, or writes solely to paths this project owns — a reconciler writing into an upstream-owned data dir gets none, because aReadWritePathsthat misses a path fails at the write rather than at start. Nothing is added to units nixpkgs owns. The notify token reaches non-root consumers via systemdLoadCredential. - WireGuard, not public exposure. That is the way in.
What it will NOT do: your responsibility
- The root of trust. It generates its own runtime secrets, but any secret you supply (sops/age, …) and the keys that decrypt them are yours to store, back up, and rotate. The framework never sees your master key.
- Back up generate-once keys. A
generateOncekey is safe from silent replacement, but the framework does not back it up. Lose it while its data survives and that data is unrecoverable. Copy these out-of-band. This is the sharpest edge here. - Harden the host. SSH, firewall baseline, kernel and account hardening, disk encryption: all the host’s job. The framework hardens its service sandboxes, not your machine.
- Make the exposure decision. Putting a service on the public internet is your call and your risk, a deliberate out-of-scope choice. Nothing here lightens it.
- Guard a misconfigured edge. Backends bind localhost so the proxy is the only path in. Keep it that way.
Any custom edge or proxy-auth you wire must strip client-supplied identity headers, or they are spoofable.
A container published on
0.0.0.0breaks the invariant quietly, since podman and docker DNAT ahead of the firewall’s input rules. See Containers.
Reading a secret
homelab-secrets is the read-only way in, so you do not have to remember which of the two directories a
secret lives in or what it is called. With no argument it lists the apps holding secrets:
$ homelab-secrets ls
APP SECRETS STATUS
coturn 2 ok
pocket-id 2 ok
radicale 3 1 drift
sonarr 3 ok
An argument matches both app and name, so ls radicale drills into one app and ls alice finds every
secret belonging to one person. cat is the only command that needs root, and it takes the name ls
printed:
$ sudo homelab-secrets cat radicale-password-alice
STATUS is worth reading. missing means the file is not there, and the error from cat names the unit
that should have generated it. unknown means the file sits under a root-only directory, so its state is
unknowable from where you are rather than absent. Running ls as yourself still gives you the whole
inventory, and it says how many rows a sudo ls would resolve. drift means the owner or mode on disk is
not what was declared, which is what a hand-restored key with the wrong ownership looks like — ls -l
shows both values. The generator fixes the mode on its next run, but it is a RemainAfterExit oneshot, so
that run is the next reboot unless you restart it yourself.
Handing out a generated account password
Radicale, CouchDB and the SMB server authenticate people directly, because their clients cannot do SSO. The
framework generates a password per enabled user instead of asking you for one, root-owned 0400 under
runtimeSecretsDir:
$ sudo homelab-secrets cat radicale-password-alice
Read it once and hand it over out of band. Nothing mails it. To roll one, delete the file and restart
homelab-runtime-secrets.service.
Key rotation
| Secret | How it rotates |
|---|---|
| OIDC client secrets | Framework-managed: oidc-rotate [<client>] (always available) or the opt-in rotation timer. It removes the secret and the provider re-mints it. |
Random per-service secrets (regenerateIfMissing) | Delete the file, then systemctl restart homelab-runtime-secrets.service. A rebuild alone will not do it: the unit is a RemainAfterExit oneshot, so systemd leaves it alone until its definition changes. |
Data-bound keys (generateOnce) | Manual and deliberate. Rotating means re-keying the data it protects, so the framework refuses to auto-rotate (that would brick the data). Remove the secret together with the data at the path generateOnce names, then restart the unit above. |
Externally-synced secrets (regenerateIfMissing = false) | Rotate in your own store. The framework leaves them untouched. |
Restore & disaster recovery
There is deliberately no restore command. In a real disaster the host is gone, and any wrapper it built with it, so recovery is a manual runbook you must be able to run from nothing but your backups and your out-of-band keys. Rehearse it before you need it.
What the backups contain. Each selfhost.backup.targets.<name> is an independent rustic repository. On the
live host its profile is written to /etc/rustic/<name>.toml (repository + password-file, plus a
<name>-secrets.toml for backend credentials). A snapshot holds only what was staged into that target’s tree:
your bindings (paths mounted read-only) and each hook’s output under extras/<hook> (e.g. a Gitea repo copy,
a DB dump). A hook output is material to replay, not a live service. Restoring a DB dump means importing it,
not dropping it onto a running database.
WireGuard peers are runtime state. /var/lib/wireguard/peers.json is the only inventory of who may
connect, and it is not in your repo. The app’s backup hook covers it. The server private key is
deliberately left out, so restoring the file gets every device its address and public key back while the
server presents a new one: each client then needs its [Peer] PublicKey edited, not a re-enrolment.
What is NOT in them. Runtime secrets (/var/lib/homelab-secrets) and OIDC credentials are not backed up
unless you explicitly add them as a binding. Two things you must hold out-of-band, or the rest is
unrecoverable:
- your secrets-backend master key (sops/age). Without it none of the secrets you supply decrypt.
- every
generateOncekey (e.g. Pocket-ID’s encryption key). Lose one while its data survives and that data is gone. This is the sharpest edge in the whole system.
Recovery order.
- Rebuild the host and restore the secrets backend, putting the sops/age master key back first.
- Restore generate-once keys from your out-of-band copy into
runtimeSecretsDirbefore their services start, so they decrypt existing data instead of the guard leaving them absent. nixos-rebuild switch. Disposable secrets regenerate, and OIDC users and clients re-provision.- Restore data with rustic into each service’s data location, then replay hook outputs (import dumps,
drop repos back). List and pull a snapshot with
rustic -P <name> snapshots/rustic -P <name> restore <id> --to <dest>. On a bare machine, reconstruct the repo string, password, and backend credentials by hand from your secrets store first.