diff --git a/.gitignore b/.gitignore index 86829e9..695aa96 100644 --- a/.gitignore +++ b/.gitignore @@ -13,11 +13,3 @@ 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 deleted file mode 100644 index c3f4ce1..0000000 --- a/.sops.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# 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/README.md b/README.md index 76cc987..43fbc08 100644 --- a/README.md +++ b/README.md @@ -291,12 +291,6 @@ top of the human-facing docs, not a replacement for them. Vue/Quasar UMD traps in lnbits page templates: no self-closing tags, CSS specificity vs Quasar's `!important` utilities, cache busting via `?v={server_startup_time}`, dark-mode color discipline. -- [`docs/secrets-management.md`](docs/secrets-management.md) — - beginner-friendly walkthrough for getting secrets out of `.env` - and into sops-encrypted YAML files: generating an age key, adding - recipients, declaring secrets in NixOS, rotating, multi-host - server setups, and common pitfalls. The scaffold ships the sops-nix - wiring already (inert until you create your first encrypted file). ## Contributing to this scaffold diff --git a/configuration.nix b/configuration.nix index 164455b..8e2d9a5 100644 --- a/configuration.nix +++ b/configuration.nix @@ -24,9 +24,6 @@ # 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/docs/secrets-management.md b/docs/secrets-management.md deleted file mode 100644 index b94509e..0000000 --- a/docs/secrets-management.md +++ /dev/null @@ -1,313 +0,0 @@ -# Secrets management with sops-nix - -A beginner-friendly walkthrough of getting secrets out of your -`.env` files and into `git` safely, using -[sops-nix](https://github.com/Mic92/sops-nix). Assumes no prior -familiarity with sops, age, or NixOS secret management. - -## Why bother - -Most LNbits dev setups start with secrets in `.env`: - -```env -LNBITS_ADMIN_KEY=changeme-real-key-goes-here -POSTGRES_PASSWORD=changeme-real-password-goes-here -``` - -This works, but `.env` files have two failure modes: - -1. **They land in git accidentally.** A `git add -A` in the wrong - moment, a forgotten `.gitignore` line, and the key history-leaks - into a public repo. Rotating after the fact is painful — there's - no fixing what's already in `git log`. -2. **They live as plaintext on disk.** A backup of your dev box, a - shared filesystem snapshot, a forgotten copy in `~/Downloads/` — - all paths to the same compromise. - -sops + age fixes both. Secret values get encrypted into a YAML file -checked into your repo. The encrypted file is safe to commit, push, -and back up. Decryption happens transparently at NixOS activation -time on the host that has the matching private key. Each secret -becomes a file under `/run/secrets/` that your services read. - -## What's in the box (lnbits-sensei scaffold) - -This repo already wires sops-nix: - -- **`flake.nix`** declares `sops-nix` as a flake input. -- **`modules/secrets.nix`** imports the NixOS module and points it at - `secrets/${hostName}.yaml` + your private key at - `~/.config/sops/age/keys.txt`. -- **`.sops.yaml`** declares the recipients (which public keys can - decrypt files under `secrets/`). -- **`secrets/`** is gitignored except for `*.yaml` (which is - encrypted) and `README.md`. -- **`configuration.nix`** imports `modules/secrets.nix`. - -All of this is **inert until you create your first encrypted file**. -`nix flake check` stays green meanwhile because the module gates on -`builtins.pathExists`. - -## Step-by-step - -### 1. Install the tools - -`sops` and `age` ship in nixpkgs: - -```nix -# in modules/packages.nix or home.nix -environment.systemPackages = with pkgs; [ sops age ]; -``` - -After `nixos-rebuild switch`, `sops` and `age-keygen` are on PATH. - -For an ad-hoc install without rebuilding: - -```sh -nix-shell -p sops age -``` - -### 2. Generate your age key - -The age private key is the one piece of state that lives outside git -— it's how this machine decrypts secrets at activation time. Generate -it once per host: - -```sh -mkdir -p ~/.config/sops/age -age-keygen -o ~/.config/sops/age/keys.txt -``` - -The file looks like: - -``` -# created: 2026-05-26T... -# public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -AGE-SECRET-KEY-1YYYY... -``` - -The line starting with `AGE-SECRET-KEY-` is your **private key**. -**Back it up somewhere safe** (a password manager, a hardware key, a -trusted offline drive). If you lose it, you lose access to every -secret encrypted to it — there is no recovery. - -Print just the **public key** (you'll paste this into `.sops.yaml`): - -```sh -age-keygen -y ~/.config/sops/age/keys.txt -# age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -``` - -### 3. Add your public key to `.sops.yaml` - -Open `.sops.yaml` at the repo root. Replace the placeholder -`age1REPLACEME...` with your real public key from the previous step: - -```yaml -keys: - - &admin age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - -creation_rules: - - path_regex: secrets/.*\.yaml$ - key_groups: - - age: - - *admin -``` - -What this says: any file matching `secrets/*.yaml` should be -encrypted for the recipients listed under `key_groups.age`. Right -now that's just `*admin` (a YAML anchor pointing at your key). When -you have collaborators, you'll add their public keys as additional -recipients here. - -### 4. Create your first encrypted secrets file - -`sops` is the editor wrapper. It opens your `$EDITOR` on a plaintext -view of the file; on save it encrypts in place. - -```sh -sops secrets/.yaml -``` - -(Use the same `hostName` you put in `settings.nix` — that's what -`modules/secrets.nix` looks for.) - -Your editor opens an empty buffer. Write your secrets as plain YAML: - -```yaml -lnbits-admin-key: changeme-real-key-goes-here -postgres: - lnbits-password: changeme-real-password-goes-here -``` - -Save and exit. sops encrypts in place. Cat the file to see what -landed on disk: - -```sh -cat secrets/.yaml -``` - -You'll see the keys are now opaque base64 blobs, and a `sops:` -metadata block at the bottom describes which recipients can decrypt. -This file is **safe to commit and push**. - -### 5. Declare each secret in your NixOS config - -For each secret you want exposed to a service, add a -`sops.secrets.` declaration in the consuming module: - -```nix -# in the module that consumes the secret -{ config, ... }: -{ - sops.secrets.lnbits-admin-key = { - mode = "0400"; - owner = config.lnbits-sensei.user; - }; -} -``` - -The `` matches the YAML key in your encrypted file. Dots in -the key become nested paths under `/run/secrets/`: - -| YAML key | Runtime file | -|---|---| -| `lnbits-admin-key` | `/run/secrets/lnbits-admin-key` | -| `postgres/lnbits-password` (nested) | `/run/secrets/postgres/lnbits-password` | - -### 6. Reference the secret from a service - -Read the runtime path via `config.sops.secrets..path`: - -```nix -# example: passing the LNbits admin key file to a service -services.someThing.adminKeyFile = - config.sops.secrets.lnbits-admin-key.path; -``` - -Use a file-path reference (not the value directly) wherever the -service supports one — that way the secret never ends up in the -Nix store, just at `/run/secrets/` on the running host. - -### 7. Activate - -```sh -sudo nixos-rebuild switch --flake .# -``` - -At activation, sops-nix decrypts `secrets/.yaml` using the -private key at `~/.config/sops/age/keys.txt`, writes each declared -secret as a file under `/run/secrets/`, and your service reads from -the file path. Done. - -## Common operations - -**Edit a secret:** - -```sh -sops secrets/.yaml -# editor opens with plaintext view; sops re-encrypts on save -``` - -**View a secret without editing:** - -```sh -sops -d secrets/.yaml | grep lnbits-admin-key -``` - -**Rotate to new recipients (e.g. add a collaborator):** - -1. Add their public key to `.sops.yaml`: - ```yaml - keys: - - &admin age1xxx... - - &alice age1zzz... - creation_rules: - - path_regex: secrets/.*\.yaml$ - key_groups: - - age: - - *admin - - *alice - ``` -2. Re-encrypt every file to the new recipient set: - ```sh - sops updatekeys secrets/.yaml - ``` - -**Remove a recipient:** edit `.sops.yaml`, then `sops updatekeys`. -**Also rotate the underlying secret values** — anything the removed -party could previously decrypt is now compromised. - -## Multi-host / server deployment - -When you have one repo deploying multiple machines, each machine -needs its own age private key, and each machine's secrets file -needs to be encrypted to that machine's public key in addition to -the operator's. - -**On each NixOS host (one-time):** - -```sh -sudo mkdir -p /var/lib/sops-nix -sudo age-keygen -o /var/lib/sops-nix/key.txt -sudo chmod 600 /var/lib/sops-nix/key.txt -sudo age-keygen -y /var/lib/sops-nix/key.txt -# copy this public key into .sops.yaml as a new recipient -``` - -In `modules/secrets.nix`, swap `age.keyFile` for the host-local path: - -```nix -sops.age.keyFile = "/var/lib/sops-nix/key.txt"; -``` - -Then in `.sops.yaml`, list every host's public key alongside the -operator's, scoped via `path_regex` if you want per-host isolation: - -```yaml -keys: - - &admin age1xxx... # operator (laptop / dev box) - - &host-a age1aaa... # server A - - &host-b age1bbb... # server B -creation_rules: - - path_regex: secrets/host-a\.yaml$ - key_groups: - - age: - - *admin - - *host-a - - path_regex: secrets/host-b\.yaml$ - key_groups: - - age: - - *admin - - *host-b -``` - -That way `host-a` can only decrypt its own secrets, `host-b` only -its own, and the operator can decrypt both. - -## Pitfalls - -- **Don't commit unencrypted YAML under `secrets/`.** The pre-commit - hook most consumers use catches obvious cases, but it's not - foolproof. Always verify with `cat secrets/.yaml` that you - see `sops:` metadata + base64 blobs, not plaintext. -- **Don't lose the private key.** No recovery. Back up the file at - `~/.config/sops/age/keys.txt` (or `/var/lib/sops-nix/key.txt` on - servers) somewhere safe and offline. -- **`updatekeys` re-encrypts but doesn't rotate secret values.** If - you're removing a recipient because they shouldn't have access - anymore, also change the underlying secrets — they could've - cached the decrypted values while they had access. -- **`sops -d` prints plaintext.** Don't pipe it into anything that - logs (`tee`, journalctl, history-aware shells). Use the file path - via `config.sops.secrets..path` in NixOS config; never - hardcode the decrypted value. -- **`environment.etc` writes to the Nix store** — readable by every - user on the host. Use `sops.secrets.` (writes to - `/run/secrets/` with mode-bits you control) instead. -- **`builtins.pathExists` is checked at eval time.** If you delete - `secrets/.yaml` after a successful build, the next - rebuild silently drops the sops block — the failure surfaces only - when a service tries to read a missing `/run/secrets/` file. - Don't delete a host's encrypted file unless you also remove every - `sops.secrets.` referencing it. diff --git a/flake.lock b/flake.lock index 23fb283..b1dafb6 100644 --- a/flake.lock +++ b/flake.lock @@ -56,28 +56,7 @@ "inputs": { "home-manager": "home-manager", "lnbits-src": "lnbits-src", - "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" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 5d3144f..7a9f981 100644 --- a/flake.nix +++ b/flake.nix @@ -21,17 +21,6 @@ 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 deleted file mode 100644 index 9f6fc9c..0000000 --- a/modules/secrets.nix +++ /dev/null @@ -1,42 +0,0 @@ -# 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 deleted file mode 100644 index 0d675f0..0000000 --- a/secrets/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# 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`.