From f9358375b5ce15061f294a9710befb0594842958 Mon Sep 17 00:00:00 2001 From: erik Date: Sun, 12 Jul 2026 20:48:33 +0200 Subject: [PATCH] feat: sops decrypts via pre-generated SSH host key (works on boot #1) - pre-generate jupiter SSH host key off-repo (~/.config/homelab/jupiter) - derive age recipient via ssh-to-age, add to .sops.yaml, re-encrypt secret - secrets.nix: sops.age.sshKeyPaths = host key (drop separate keyFile) - gitignore private-key patterns - README: ship host key via nixos-anywhere --extra-files; secret decrypts boot #1 Co-Authored-By: Claude Opus 4.8 --- .gitignore | 7 +++++++ .sops.yaml | 5 +++++ README.md | 35 ++++++++++++++++++++++++++++------- jupiter/secrets.nix | 12 +++++++----- secrets/jupiter.yaml | 19 ++++++++++++++----- 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 753c953..8b25fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ result-* # built artifacts *.ova + +# private keys — must never be committed (they live off-repo in ~/.config) +*_ed25519_key +*.pem +id_* +age-keys.txt +keys.txt diff --git a/.sops.yaml b/.sops.yaml index 4394256..686d572 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -6,9 +6,14 @@ # Add the host's own age key here later (derived from its ssh host key) so the # machine can decrypt without shipping a separate key. keys: + # admin key — for editing secrets on the laptop (~/.config/sops/age/keys.txt) - &admin age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2 + # jupiter host key — derived from its pre-generated SSH host key with + # ssh-to-age. The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key. + - &jupiter age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt creation_rules: - path_regex: secrets/.*\.yaml$ key_groups: - age: - *admin + - *jupiter diff --git a/README.md b/README.md index a89b8f0..f2d5198 100644 --- a/README.md +++ b/README.md @@ -37,17 +37,32 @@ an installer, partitions via disko, installs. 1. Set the real OS disk id in `jupiter/disk-config.nix` (`ls -l /dev/disk/by-id`), and the data-disk mount in `configuration.nix`. -2. Add your SSH pubkey to `users.users.darman.openssh.authorizedKeys.keys`. -3. Wire the samba secret (see Notes) — real password, not the VM's plaintext. -4. Run from your laptop: +2. Add your login SSH pubkey to `users.users.darman.openssh.authorizedKeys.keys`. +3. Set the real samba password: + ``` + export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt + nix shell nixpkgs#sops -c sops secrets/jupiter.yaml # edit, commit + ``` +4. Stage the pre-generated host key so sops can decrypt on boot #1 + (private key lives off-repo in `~/.config/homelab/jupiter/`): + ``` + install -Dm600 ~/.config/homelab/jupiter/ssh_host_ed25519_key \ + /tmp/extra/etc/ssh/ssh_host_ed25519_key + install -Dm644 ~/.config/homelab/jupiter/ssh_host_ed25519_key.pub \ + /tmp/extra/etc/ssh/ssh_host_ed25519_key.pub + ``` +5. Run from your laptop: ``` nix run github:nix-community/nixos-anywhere -- \ --flake .#jupiter \ + --extra-files /tmp/extra \ --generate-hardware-config nixos-generate-config ./jupiter/hardware-configuration.nix \ --target-host root@ ``` - `--generate-hardware-config` pulls the target's real kernel modules into the - placeholder for you. Commit the result. Reboot into NixOS. + `--extra-files` plants the host key before first boot (its age identity is + already a recipient in `.sops.yaml`, so `/run/secrets/samba_password` + decrypts on boot #1). `--generate-hardware-config` pulls the target's real + kernel modules into the placeholder. Commit the result. Reboot into NixOS. Manual alternative (USB ISO): boot installer, `disko` the disk, then `nixos-install --flake .#jupiter`. @@ -72,8 +87,14 @@ prefer `services.` over a container when available. Add a `caddy` - Backend is Podman with `dockerCompat` — `docker` CLI works, no daemon. - Samba keeps its own password DB. `services.samba` never sets it; a systemd - oneshot (`samba-smbpasswd`) provisions it from `/etc/samba/smb-password`. - Real host: supply that file via **sops-nix / agenix**, never commit plaintext. + oneshot (`samba-smbpasswd`) provisions it. Host reads the password from + `/run/secrets/samba_password` (**sops-nix**); the VM falls back to plaintext + `/etc/samba/smb-password`. +- Secrets: `secrets/jupiter.yaml` is age-encrypted (safe to commit) to two + recipients in `.sops.yaml` — the **admin** key (edit on laptop, + `~/.config/sops/age/keys.txt`) and the **jupiter host** key (derived from its + SSH host key via `ssh-to-age`, decrypts at runtime). Private keys live + off-repo and are gitignored. Rotate/add recipients with `sops updatekeys`. - Data disk: plain `fileSystems."/mnt/data"` in configuration.nix — kept out of disko so it is never formatted. Reference by `by-id` / `by-uuid`. - `system.stateVersion` = `26.05`, install-time schema. Do NOT bump on upgrades. diff --git a/jupiter/secrets.nix b/jupiter/secrets.nix index d41edfd..0ab31c2 100644 --- a/jupiter/secrets.nix +++ b/jupiter/secrets.nix @@ -2,14 +2,16 @@ # sops-nix secret wiring (real host only; not imported by vm.nix). # Encrypted values live in ../secrets/jupiter.yaml, decrypted at activation to -# /run/secrets/. The host needs the age PRIVATE key at the keyFile path. +# /run/secrets/. # -# Deploy the private key with the install, e.g. nixos-anywhere: -# --extra-files, placing your key at /var/lib/sops-nix/key.txt -# or later derive a host age key from its ssh host key and add it to .sops.yaml. +# The host decrypts with its OWN SSH host key (age identity derived via +# ssh-to-age, recipient listed in ../.sops.yaml). The key is pre-generated on +# the laptop and shipped once at install as /etc/ssh/ssh_host_ed25519_key +# (nixos-anywhere --extra-files) — so decryption works on boot #1 and there is +# no separate sops-only key to manage. { sops.defaultSopsFile = ../secrets/jupiter.yaml; - sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; # Decrypts to /run/secrets/samba_password (root-only by default). sops.secrets.samba_password = { }; diff --git a/secrets/jupiter.yaml b/secrets/jupiter.yaml index 2f9ba8b..a2b6179 100644 --- a/secrets/jupiter.yaml +++ b/secrets/jupiter.yaml @@ -3,13 +3,22 @@ sops: age: - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlL0E3cWNIby95Q3RBcjlm - QklPREM4cWRaelZEckFGUWpKUWExYmxrcWdrClByTVBlWDNlRGhMUkhEQzJONzFn - alJVSzRMODN3U0cyRG93T3lHZkhXTWcKLS0tIFJ2V3I0V1NwakVlc2dCUW1iMHdR - RlNsWnJJaDkvVnJnVlcyRXZmK1VkMUUKnv02m3vGjy8nTZV9ouItOEWmfZOABKDP - VCvWcSy8R0NikkQ004XrBd1WhvyrUvqC6X9f4N9208BvmyDxEpKZbw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKNTRmRDNFeitoYTU1djdV + N2lwbGdXb21kSFhPS2xoYXNaaitmV0IzdFJvCjFoc29JcE1pNVkzUXFrRVltK0RI + MXFleXFRT1NmZ0taQ0lMaUxNNC9QbWMKLS0tIFZUOGpZUVRTYVVRa21Na01rR0ow + YXV3K3c5TVpUS3VMZFdmLzZvSUxlWFkKKU7BpZzLcAvWdMnJAHJS2gOLeS/JeU9e + 1TCQgbYeFg0YHHa3Z/Vg8Q64hSyhpYYkE46dsjgSiZfCKbH4JaEPKA== -----END AGE ENCRYPTED FILE----- recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVWnFnYW8yZmhCZ05YdzVF + SS9OcHlrMkJVQmFMbWFqbXpjbEw5ZFhDbm5JClFIZGI0RmhvMTBtbVY1S20yRWFL + YysvYVBaZWtIV3NVUHpEeWxlUmIyTkEKLS0tIEZBVmdFTEJZM2k0NUtPSTFZZFd1 + dWhWTGd6Rm5STHBveTlGcjdXbzhPYkUKAcwMaOR4nnOYOfQqH3tFnUp5QTEVsovb + p5+IspDbNPFgW4BDDb+l0mtU7+ulNG71lwySTs5aOEn3zh3vqXPR8w== + -----END AGE ENCRYPTED FILE----- + recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt lastmodified: "2026-07-12T18:10:45Z" mac: ENC[AES256_GCM,data:ynQU/B5RvwGw1ILJJexrZWtzNr7z3IUyy86v8q0KhRsMH2qSHF0BK0Pzq/DcT3vru+MwKat1hJfO21Pdu+HRsqtzUZRViTCbjtz+vmnjwyQvBqBPhEtxDeBoTgd5RqjXxzcuOADfv0NkfvU9p06x8YF8AVYq9ONvOEZiC4MamoY=,iv:1Wm4JP5Djyc9jdExMBcSjWw/zF1EXexjjMhePOEWCt8=,tag:A51vbK8fpb8sg+eCLwbWmg==,type:str] unencrypted_suffix: _unencrypted