Compare commits
No commits in common. "fc1d31244ab19f05bdef85dbfb34c17c993ff0fe" and "8ebf16d06968de49829629fb91fda192bcffee63" have entirely different histories.
fc1d31244a
...
8ebf16d069
9 changed files with 1 additions and 465 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
21
.sops.yaml
21
.sops.yaml
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
23
flake.lock
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
11
flake.nix
11
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/<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 =
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
|
|
@ -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`.
|
||||
Reference in a new issue