Compare commits

..

No commits in common. "fc1d31244ab19f05bdef85dbfb34c17c993ff0fe" and "8ebf16d06968de49829629fb91fda192bcffee63" have entirely different histories.

9 changed files with 1 additions and 465 deletions

8
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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/<name>` 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/<hostName>.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/<hostName>.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.<name>` 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 `<name>` 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.<name>.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/<name>` on the running host.
### 7. Activate
```sh
sudo nixos-rebuild switch --flake .#<hostName>
```
At activation, sops-nix decrypts `secrets/<hostName>.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/<hostName>.yaml
# editor opens with plaintext view; sops re-encrypts on save
```
**View a secret without editing:**
```sh
sops -d secrets/<hostName>.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/<hostName>.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/<file>.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.<name>.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.<name>` (writes to
`/run/secrets/` with mode-bits you control) instead.
- **`builtins.pathExists` is checked at eval time.** If you delete
`secrets/<hostName>.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/<name>` file.
Don't delete a host's encrypted file unless you also remove every
`sops.secrets.<name>` referencing it.

23
flake.lock generated
View file

@ -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"
}
}
},

View file

@ -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/<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 =

View file

@ -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";
};
}

View file

@ -1,39 +0,0 @@
# 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`.