Compare commits

...
Author SHA1 Message Date
finn.markwitz 621ca5dde6 docs: sops-nix secrets workflow
Operator runbook for adding a secret, adding a new host, and rotating
keys, plus a pointer from CLAUDE.md's architecture overview.
2026-09-08 15:43:50 +02:00
finn.markwitz a00916864c feat(mibook): decrypt placeholder secret via host age key
Proves the sops-nix pipeline end to end on mibook: encrypts a
throwaway secret to the operator + mibook host keys, declares it in
machines/mibook/secrets.nix, and verifies it decrypts to
/run/secrets/placeholder (root:root, 0400) on nixos-rebuild switch.

Also removes the dead `let secrets = config.sops.secrets;` binding
from both environments.nix files — leftover from an earlier abandoned
plan, never referenced.
2026-09-08 15:43:45 +02:00
finn.markwitz 3ab8bfbb91 feat(sops): add sops-nix input, core module and devShell tooling
Wires sops-nix into every machine via machines/core/sops.nix so hosts
decrypt with their existing SSH host key. Adds a devShell exposing
sops/age/ssh-to-age for the operator workflow, which also fixes .envrc's
previously no-op `use flake`.
2026-09-08 15:43:36 +02:00
finn.markwitz 70deb25080 chore: Flake Update 2026-08-21 10:30:01 +02:00
finn.markwitz ac57abf255 chore: Flake Update 2026-08-11 15:40:07 +02:00
finn.markwitz 83ee518091 Merge pull request 'feat(jupiter): native Immich profile module (docker → NixOS)' (#10) from feat/immich-nixos-module into main
Reviewed-on: #10
2026-08-07 11:13:40 +02:00
14 changed files with 253 additions and 34 deletions
+3 -1
View File
@@ -1 +1,3 @@
AGENTS.md
AGENTS.md
*.dec
keys.txt
+17
View File
@@ -0,0 +1,17 @@
keys:
- &admin_finn age1a0v442ct2qfjcdhyktn25spw02yq4fl08plrrvuradwwxzsle4mq8thln4
- &host_mibook age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p
# - &host_jupiter age1... # TODO: Phase 9, see docs/secrets.md
creation_rules:
- path_regex: ^secrets/mibook\.yaml$
key_groups:
- age:
- *admin_finn
- *host_mibook
# - path_regex: ^secrets/jupiter\.yaml$ # TODO: Phase 9
# key_groups:
# - age: [ *admin_finn, *host_jupiter ]
# - path_regex: ^secrets/common\.yaml$ # TODO: Phase 9
# key_groups:
# - age: [ *admin_finn, *host_mibook, *host_jupiter ]
+12
View File
@@ -35,6 +35,7 @@ flake.nix
│ ├── core.nix # system packages, locale, timezone
│ ├── network.nix
│ ├── nix.nix
│ ├── sops.nix # sops-nix decryption config, sops/age/ssh-to-age on PATH
│ └── users.nix
├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*)
│ ├── environments/ # per-service/app profiles
@@ -43,10 +44,21 @@ flake.nix
└── machines/<name>/
├── configuration.nix # machine-specific NixOS settings
├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options
├── secrets.nix # sops.secrets declared for this host
├── disks.nix
└── hardware-configuration.nix
```
### Secrets (sops-nix)
Secrets are encrypted with sops, decrypted per-host from that host's SSH host
key. `.sops.yaml` defines who can decrypt what; `secrets/<host>.yaml` holds
the encrypted values; `machines/<host>/secrets.nix` declares which secrets
that host exposes and consumes them via `config.sops.secrets.<name>.path`.
Always `git add` a secrets file before `nix build` — untracked files are
invisible to flake evaluation. Full workflow (adding a secret, adding a host,
rotating keys): see [`docs/secrets.md`](docs/secrets.md).
### Profile / Module Pattern
Every module under `modules/` follows the same structure:
+92
View File
@@ -0,0 +1,92 @@
# Secrets (sops-nix)
Secrets are encrypted with [sops](https://github.com/getsops/sops) and decrypted
at activation by [sops-nix](https://github.com/Mic92/sops-nix). Each host decrypts
with its own SSH host key (converted to an age key in-memory — no separate host
private key is ever stored on disk).
Tooling (`sops`, `age`, `ssh-to-age`) is available via `nix develop` in this repo,
and installed system-wide on every machine via `machines/core/sops.nix`.
## Adding a secret to an existing host
```
nix develop -c sops secrets/<host>.yaml
```
This opens `$EDITOR` on the decrypted content. Add your key, save, quit — sops
re-encrypts on write. Then declare it in `machines/<host>/secrets.nix`:
```nix
sops.secrets.my-secret = {
owner = "someuser"; # defaults to root
group = "someuser";
mode = "0400";
restartUnits = [ "foo.service" ]; # if a running service needs to pick it up
};
```
Consume it in the owning module as `config.sops.secrets.my-secret.path`
(resolves to `/run/secrets/my-secret`) — never inline the decrypted value into
a Nix string, since that copies it into the world-readable `/nix/store`.
Then:
```
nix build '.#nixosConfigurations.<host>.config.system.build.toplevel'
sudo nixos-rebuild test --flake '.#<host>'
# verify, then:
sudo nixos-rebuild switch --flake '.#<host>'
```
**The secrets file must be `git add`ed before `nix build`** — untracked files
are invisible to flake evaluation, and `sops.validateSopsFiles` needs it in the
Nix store.
## Adding a new host
1. On the new host: `ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub` to get its
age public key.
2. In `.sops.yaml`: add a `&host_<name>` anchor with that key, and a
`creation_rules` entry for `secrets/<name>.yaml` including both the new
host key and `*admin_finn`.
3. Create `machines/<name>/secrets.nix`, add it to that machine's
`configuration.nix` imports.
4. If the host needs to read an existing shared file, run
`sops updatekeys secrets/<file>.yaml` after step 2 so it's re-encrypted to
include the new recipient.
## Rotating keys
```
# after editing .sops.yaml (new/removed recipient):
nix develop -c sops updatekeys secrets/<file>.yaml
```
for every file the change affects. `updatekeys` requires you to already be
able to decrypt the file — this is why `*admin_finn` is a recipient on every
rule, as the standing fallback.
Reinstalling a host regenerates its SSH host key, which changes its age
identity. Any file previously encrypted to that host must be `updatekeys`'d
with the new key, or the host's next activation fails to decrypt.
## Where private keys live
- **Operator** (you): `~/.config/sops/age/keys.txt` — sops' default lookup
path, so `SOPS_AGE_KEY_FILE` never needs to be set. Never committed. Back
this up out-of-band (password manager, offline copy) — if it's lost, only
the surviving host keys can still decrypt their own files, and any file that
was only ever readable by an operator-only rule becomes permanently
unreadable.
- **Hosts**: `/etc/ssh/ssh_host_ed25519_key` — already managed by NixOS,
converted to age in-memory at activation time. Nothing extra to back up.
## Layout
```
.sops.yaml # age recipients + creation rules (which keys can decrypt which files)
secrets/<host>.yaml # per-host encrypted secrets (default sopsFile for that host)
machines/core/sops.nix # applied to every machine: decryption config, sops/age/ssh-to-age on PATH
machines/<host>/secrets.nix # per-host: which secrets exist, owner/group/mode
```
Generated
+42 -21
View File
@@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1784762557,
"narHash": "sha256-R/r6jRnANV50c8F5Fz5+1Q1moab0IGWRk+cg5ME2nMY=",
"lastModified": 1787274306,
"narHash": "sha256-Qg9f9td5iphUWSQS6zmvyZWO1F+D7j8Z3U6dGyUTg08=",
"owner": "NixOS",
"repo": "nix",
"rev": "d10c84cd0cc0efdcb29cf2611caf5fbcd10fa071",
"rev": "649e823fb24ed118d72e613be35fa8ea1b64afe7",
"type": "github"
},
"original": {
@@ -143,11 +143,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1784723954,
"narHash": "sha256-1CfD8ZUjCkTgjsneLZ/lxCHhgDfqxxE7/GX0MmsgiqA=",
"lastModified": 1787144466,
"narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "a017f5b72210026af5b3ac5949f08d94380a6fbd",
"rev": "0471accf8d0a8210b31d947497d179ecc99e0021",
"type": "github"
},
"original": {
@@ -187,11 +187,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1782614948,
"narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
"lastModified": 1785031560,
"narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
"rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c",
"type": "github"
},
"original": {
@@ -218,11 +218,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1784796856,
"narHash": "sha256-vwxWgF+Gj276WznzGb1LxGsK/39HaQwgQXiU3EkC844=",
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
"lastModified": 1787135253,
"narHash": "sha256-M5/r2v++FjVhdsxXYMb4BDJ5YLAdCWFt3aZotcshocA=",
"rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1040357.e2587caef70c/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1058091.ffb3c9b700e7/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
@@ -245,11 +245,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1784707089,
"narHash": "sha256-DUedXhD2Rg8q4Xyd07Sb90eZGy4gg6W+Vl/WbLNwAZo=",
"rev": "b3fe9581c9061c749abef42b6d4ee7b7c05c33fa",
"lastModified": 1787101114,
"narHash": "sha256-BA7sSNjLDuPGSOYBGpr6WQjke1MQ8AZpJ8GlYZM/mOc=",
"rev": "b18a4b905f8d028dc4476412e6d6891728695379",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.5845.b3fe9581c906/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.8045.b18a4b905f8d/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
@@ -264,7 +264,28 @@
"nixos-generators": "nixos-generators",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs_3",
"nixpkgs-unstable": "nixpkgs-unstable"
"nixpkgs-unstable": "nixpkgs-unstable",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1788337237,
"narHash": "sha256-gkSH8VUtCo6hnysNmb9DbTuDepH2t5pv+QWjP75xKAk=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "fbf759290e0cb0a98dfc813a4eb7d53ad1dacb57",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
}
},
+13
View File
@@ -11,6 +11,10 @@
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -36,6 +40,15 @@
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
sops
age
ssh-to-age
nixfmt
];
};
};
systems = [ "x86_64-linux" ];
+2
View File
@@ -4,6 +4,7 @@ let
nixpkgs
nixpkgs-unstable
nixos-hardware
sops-nix
#hyprland
;
nixosSystem = nixpkgs.lib.makeOverridable nixpkgs.lib.nixosSystem;
@@ -40,6 +41,7 @@ let
];
}
../modules
sops-nix.nixosModules.sops
# ../profiles
];
defaultModules = baseModules ++ customModules;
+1
View File
@@ -3,5 +3,6 @@
./modules.nix
./network.nix
./nix.nix
./sops.nix
./users.nix
]
+31
View File
@@ -0,0 +1,31 @@
# sops-nix plumbing — applied to every machine.
# See docs/secrets.md for the day-to-day workflow.
{
config,
lib,
pkgs,
...
}:
{
# Each host defaults to its own secrets/<hostname>.yaml. Override per-secret
# with sops.secrets.<name>.sopsFile for shared files.
sops.defaultSopsFile = lib.mkDefault (../../secrets + "/${config.networking.hostName}.yaml");
sops.defaultSopsFormat = "yaml";
sops.validateSopsFiles = true;
# Decrypt with the host's own SSH host key, converted to age.
# Explicit rather than inheriting the openssh-derived default, so this does
# not silently break if services.openssh is ever disabled.
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.age.generateKey = false;
# Age only. The default here is the RSA host key imported as a GPG key,
# which is deprecated and an extra failure surface.
sops.gnupg.sshKeyPaths = [ ];
environment.systemPackages = with pkgs; [
sops
age
ssh-to-age
];
}
+1 -5
View File
@@ -1,9 +1,5 @@
# enabled profiles
{ config, lib, ... }:
let
secrets = config.sops.secrets;
in
{
_: {
my.profiles = {
hyprland.enable = false;
zsh.enable = true;
+1 -2
View File
@@ -11,6 +11,7 @@
./hardware-configuration.nix
./environments.nix
./network.nix
./secrets.nix
# ./system.nix use docker here
];
@@ -21,7 +22,6 @@
useOSProber = true;
};
# Configure keymap in X11
services.xserver.xkb = {
layout = "de";
@@ -35,7 +35,6 @@
services.printing.enable = true;
nixpkgs.config.allowUnfree = true;
hardware.nvidia.prime = {
sync.enable = false;
+1 -5
View File
@@ -1,9 +1,5 @@
# enabled profiles
{ config, lib, ... }:
let
secrets = config.sops.secrets;
in
{
_: {
my.profiles = {
kde-desktop.enable = true;
zsh.enable = true;
+12
View File
@@ -0,0 +1,12 @@
# Secrets decrypted on mibook. sopsFile defaults to secrets/mibook.yaml
# via machines/core/sops.nix.
{ ... }:
{
# Pipeline smoke test — proves host-key decryption works end to end.
# Safe to delete once a real secret is in place.
sops.secrets.placeholder = {
mode = "0400";
owner = "root";
group = "root";
};
}
+25
View File
@@ -0,0 +1,25 @@
placeholder: ENC[AES256_GCM,data:tji/6di1F2GsD60TEBGAVvZR9m4jxQ8=,iv:BoBQVR735vq38iGx40VHIpk185ylf79mKmnpewcq/+o=,tag:lXNNk4/HYB0sw44KaQ2KYg==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZUk5cjhGTlVveUFRdHdS
elpVV0pZVU54U3plYjNBai9ZZVBxTDhyRFFnCmpGWW81VGJoMnd4eWxCWnovQnFF
WXR1MVBTZ3VRUnNMUjI5Rko4NHpBdjQKLS0tIDkxYWNxTmhUS2dYTkFnTWZXaS9V
ejdtSmF2M1ZVdFNiWENMWlNraUYxVzAKdlz4xLhTKjJfFESDcym2xCOa71rXj6BE
mdeaCDhE11CAXqrpKU+j0/kwTsZRzyqlwQsfeI4ratu1K1vY66ONsw==
-----END AGE ENCRYPTED FILE-----
recipient: age1a0v442ct2qfjcdhyktn25spw02yq4fl08plrrvuradwwxzsle4mq8thln4
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYT1hRSGt6TDRSMklQNDA3
Z2lYS2pMRWxxeEtGRGJSeXRDSlY5UTFBNEZvCm5RK0huT282MXpwWjdrTHByY3Ev
bTBMczR1MzVEcnNDVUxadlREbGg3UUUKLS0tIHhydUlmRExkbmNNNkVTcW1OSnpZ
aEtLZkRTRE9GZ0I1bEN0ZEl3RjVMeDgKoCY7NyxFMS+WuBUez2Irf91Xuy9QpM5g
RTmhK6mw2wd8oUMdgPOUESNwh1xkCz4hDHPrDQnfd4sPhSAvh28jwg==
-----END AGE ENCRYPTED FILE-----
recipient: age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p
lastmodified: "2026-09-08T13:29:12Z"
mac: ENC[AES256_GCM,data:YjMMIUdfCl8dkuvtvBZTFIhLwsUjI6iSeN929O4igbwpRTyGVxgLmAMl81YYHJSyEibelT7ZBKiuVVnL2KjLvAKTj3H1O+AQrcAEJDPC9NwPJwbtAIpLl2li5060Kdj4kr8AY+UJ2kHycuS9dg6wPZTLBJcM/YCw0AoVftvzryM=,iv:GEQZF9RvW+MJlMIjn1ToD74RubWMQtCyE5LqW9KQQfA=,tag:NR6u88eBY1asQZM6f45hTw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3