From 7af3bce544ebf28ec6b971c1c9fc4cc91b43495f Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 26 May 2026 08:44:55 +0200 Subject: [PATCH] feat(secrets): scaffold sops-nix for declarative secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 8 ++++++++ .sops.yaml | 21 +++++++++++++++++++++ configuration.nix | 3 +++ flake.lock | 23 ++++++++++++++++++++++- flake.nix | 11 +++++++++++ modules/secrets.nix | 42 ++++++++++++++++++++++++++++++++++++++++++ secrets/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 .sops.yaml create mode 100644 modules/secrets.nix create mode 100644 secrets/README.md diff --git a/.gitignore b/.gitignore index 695aa96..86829e9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,11 @@ result-* .#* *.swp *.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 diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..c3f4ce1 --- /dev/null +++ b/.sops.yaml @@ -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 diff --git a/configuration.nix b/configuration.nix index 8e2d9a5..164455b 100644 --- a/configuration.nix +++ b/configuration.nix @@ -24,6 +24,9 @@ # Option schema (lnbits-sensei.*). ./modules/core.nix + # sops-nix wiring. Inert until secrets/.yaml exists. + ./modules/secrets.nix + # Git remote topology — upstream / fork / extras. ./modules/git/remotes.nix diff --git a/flake.lock b/flake.lock index b1dafb6..23fb283 100644 --- a/flake.lock +++ b/flake.lock @@ -56,7 +56,28 @@ "inputs": { "home-manager": "home-manager", "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" } } }, diff --git a/flake.nix b/flake.nix index 7a9f981..5d3144f 100644 --- a/flake.nix +++ b/flake.nix @@ -21,6 +21,17 @@ url = "github:lnbits/lnbits"; 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/. 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 = diff --git a/modules/secrets.nix b/modules/secrets.nix new file mode 100644 index 0000000..9f6fc9c --- /dev/null +++ b/modules/secrets.nix @@ -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"; + }; +} diff --git a/secrets/README.md b/secrets/README.md new file mode 100644 index 0000000..0d675f0 --- /dev/null +++ b/secrets/README.md @@ -0,0 +1,39 @@ +# secrets/ + +Encrypted YAML files in this directory are decrypted at NixOS +activation time and exposed under `/run/secrets/` for any +service that declares `sops.secrets.` 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/.yaml +# sops auto-encrypts on save using recipients from .sops.yaml + +# Later edits go through sops (auto-decrypts, re-encrypts on save) +sops secrets/.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/.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.` +and read the runtime path via `config.sops.secrets..path`.