feat(secrets): scaffold sops-nix for declarative secrets

Wires sops-nix as a flake input and bakes the NixOS module into
configuration.nix via modules/secrets.nix. Per-host defaults live in
modules/secrets.nix:

- defaultSopsFile = ../secrets/${settings.hostName}.yaml
- defaultSopsFormat = yaml
- age.keyFile = /home/${settings.user}/.config/sops/age/keys.txt

The whole sops block is gated on `builtins.pathExists` so flake eval
succeeds before the encrypted file is created — important during the
scaffold-bootstrap phase where the consumer hasn't yet generated an
age key.

Adds .sops.yaml with a placeholder admin recipient (overwrite with
your real age public key before encrypting anything) and a
creation_rules block matching `secrets/*.yaml`.

.gitignore loosened so `secrets/*.yaml` and `secrets/README.md` can
be checked in while plaintext key material (`*.key`, `*.pem`) and
anything else under `secrets/` stays ignored. The pre-commit secret
scanner most consumers use is the second line of defense.

secrets/README.md documents the workflow at the directory level.
The substantive beginner walkthrough lands in a follow-up commit at
docs/secrets-management.md.

`nix flake check --no-build` stays green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-26 08:44:55 +02:00
commit 7af3bce544
7 changed files with 146 additions and 1 deletions

8
.gitignore vendored
View file

@ -13,3 +13,11 @@ result-*
.#* .#*
*.swp *.swp
*.swo *.swo
# Secrets — track only sops-encrypted .yaml files + the README;
# block plaintext keys and any other content under secrets/
*.key
*.pem
secrets/*
!secrets/*.yaml
!secrets/README.md

21
.sops.yaml Normal file
View file

@ -0,0 +1,21 @@
# sops recipient declarations.
#
# Replace the placeholder below with YOUR age public key before
# encrypting any files. One-time setup on this machine:
# pragma: allowlist secret
# age-keygen -o ~/.config/sops/age/keys.txt # creates the private key
# age-keygen -y ~/.config/sops/age/keys.txt # prints the public key
#
# Paste the printed `age1...` string in place of the placeholder.
# See docs/secrets-management.md for the full walkthrough.
keys:
# pragma: allowlist secret
# PLACEHOLDER — overwrite with your real age public key.
- &admin age1REPLACEME_run_age_keygen_y_then_paste_the_real_key_here
creation_rules:
- path_regex: secrets/.*\.yaml$
key_groups:
- age:
- *admin

View file

@ -24,6 +24,9 @@
# Option schema (lnbits-sensei.*). # Option schema (lnbits-sensei.*).
./modules/core.nix ./modules/core.nix
# sops-nix wiring. Inert until secrets/<hostName>.yaml exists.
./modules/secrets.nix
# Git remote topology — upstream / fork / extras. # Git remote topology — upstream / fork / extras.
./modules/git/remotes.nix ./modules/git/remotes.nix

23
flake.lock generated
View file

@ -56,7 +56,28 @@
"inputs": { "inputs": {
"home-manager": "home-manager", "home-manager": "home-manager",
"lnbits-src": "lnbits-src", "lnbits-src": "lnbits-src",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1777944972,
"narHash": "sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "c591bf665727040c6cc5cb409079acb22dcce33c",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
} }
} }
}, },

View file

@ -21,6 +21,17 @@
url = "github:lnbits/lnbits"; url = "github:lnbits/lnbits";
flake = false; flake = false;
}; };
# sops-nix — declarative secrets via age-encrypted YAML files.
# Decryption happens at NixOS activation; values are exposed to
# services as files under /run/secrets/<name>. The host's age
# key lives at ~/.config/sops/age/keys.txt by default; recipients
# are declared in .sops.yaml. See modules/secrets.nix for wiring
# and docs/secrets-management.md for a walkthrough.
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = outputs =

42
modules/secrets.nix Normal file
View file

@ -0,0 +1,42 @@
# sops-nix per-host wiring.
# pragma: allowlist secret start
#
# Imports the sops-nix NixOS module and points it at this host's
# encrypted file + the consumer's age private key.
#
# - Recipients (which age public keys can decrypt) are declared in
# `.sops.yaml` at the repo root.
# - The encrypted file for this host lives at
# `secrets/${settings.hostName}.yaml`. Create it with:
# sops secrets/${settings.hostName}.yaml
# sops auto-encrypts on save using the recipients from .sops.yaml.
# - The matching private key lives at
# `/home/${settings.user}/.config/sops/age/keys.txt`. Generate it
# one-time with `age-keygen -o ~/.config/sops/age/keys.txt`.
#
# The whole sops block is gated on `builtins.pathExists` so flake
# eval succeeds before the encrypted file exists — useful for the
# scaffold-bootstrap phase. See `docs/secrets-management.md` for a
# walkthrough.
# pragma: allowlist secret end
{
config,
lib,
pkgs,
inputs,
settings,
...
}:
let
sopsFile = ../secrets/${settings.hostName}.yaml;
in
{
imports = [ inputs.sops-nix.nixosModules.sops ];
sops = lib.mkIf (builtins.pathExists sopsFile) {
defaultSopsFile = sopsFile;
defaultSopsFormat = "yaml";
age.keyFile = "/home/${settings.user}/.config/sops/age/keys.txt";
};
}

39
secrets/README.md Normal file
View file

@ -0,0 +1,39 @@
# secrets/
Encrypted YAML files in this directory are decrypted at NixOS
activation time and exposed under `/run/secrets/<name>` for any
service that declares `sops.secrets.<name>` to consume.
Recipients are declared in `../.sops.yaml`. The matching age
private key lives at `~/.config/sops/age/keys.txt` on the host
machine (see `modules/secrets.nix`).
## Workflow
```sh
# First-time: create + encrypt this host's secrets file
sops secrets/<hostName>.yaml
# sops auto-encrypts on save using recipients from .sops.yaml
# Later edits go through sops (auto-decrypts, re-encrypts on save)
sops secrets/<hostName>.yaml
```
See [`../docs/secrets-management.md`](../docs/secrets-management.md)
for the full walkthrough — generating the age key, adding a recipient,
declaring a secret in NixOS, and rotating keys.
## What goes here
One YAML file per host, named after the host. Inside each file, a
flat or nested map of secret names → values:
```yaml
# secrets/<hostName>.yaml — encrypted in place
lnbits-admin-key: changeme-real-key-goes-here
postgres:
lnbits-password: changeme-real-password-goes-here
```
NixOS modules reference these by name via `sops.secrets.<name>`
and read the runtime path via `config.sops.secrets.<name>.path`.