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 <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,10 @@ result-*
|
|||||||
|
|
||||||
# built artifacts
|
# built artifacts
|
||||||
*.ova
|
*.ova
|
||||||
|
|
||||||
|
# private keys — must never be committed (they live off-repo in ~/.config)
|
||||||
|
*_ed25519_key
|
||||||
|
*.pem
|
||||||
|
id_*
|
||||||
|
age-keys.txt
|
||||||
|
keys.txt
|
||||||
|
|||||||
@@ -6,9 +6,14 @@
|
|||||||
# Add the host's own age key here later (derived from its ssh host key) so the
|
# 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.
|
# machine can decrypt without shipping a separate key.
|
||||||
keys:
|
keys:
|
||||||
|
# admin key — for editing secrets on the laptop (~/.config/sops/age/keys.txt)
|
||||||
- &admin age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2
|
- &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:
|
creation_rules:
|
||||||
- path_regex: secrets/.*\.yaml$
|
- path_regex: secrets/.*\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin
|
- *admin
|
||||||
|
- *jupiter
|
||||||
|
|||||||
@@ -37,17 +37,32 @@ an installer, partitions via disko, installs.
|
|||||||
|
|
||||||
1. Set the real OS disk id in `jupiter/disk-config.nix`
|
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`.
|
(`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`.
|
2. Add your login SSH pubkey to `users.users.darman.openssh.authorizedKeys.keys`.
|
||||||
3. Wire the samba secret (see Notes) — real password, not the VM's plaintext.
|
3. Set the real samba password:
|
||||||
4. Run from your laptop:
|
```
|
||||||
|
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 -- \
|
nix run github:nix-community/nixos-anywhere -- \
|
||||||
--flake .#jupiter \
|
--flake .#jupiter \
|
||||||
|
--extra-files /tmp/extra \
|
||||||
--generate-hardware-config nixos-generate-config ./jupiter/hardware-configuration.nix \
|
--generate-hardware-config nixos-generate-config ./jupiter/hardware-configuration.nix \
|
||||||
--target-host root@<zimablade-ip>
|
--target-host root@<zimablade-ip>
|
||||||
```
|
```
|
||||||
`--generate-hardware-config` pulls the target's real kernel modules into the
|
`--extra-files` plants the host key before first boot (its age identity is
|
||||||
placeholder for you. Commit the result. Reboot into NixOS.
|
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
|
Manual alternative (USB ISO): boot installer, `disko` the disk, then
|
||||||
`nixos-install --flake .#jupiter`.
|
`nixos-install --flake .#jupiter`.
|
||||||
@@ -72,8 +87,14 @@ prefer `services.<app>` over a container when available. Add a `caddy`
|
|||||||
|
|
||||||
- Backend is Podman with `dockerCompat` — `docker` CLI works, no daemon.
|
- Backend is Podman with `dockerCompat` — `docker` CLI works, no daemon.
|
||||||
- Samba keeps its own password DB. `services.samba` never sets it; a systemd
|
- Samba keeps its own password DB. `services.samba` never sets it; a systemd
|
||||||
oneshot (`samba-smbpasswd`) provisions it from `/etc/samba/smb-password`.
|
oneshot (`samba-smbpasswd`) provisions it. Host reads the password from
|
||||||
Real host: supply that file via **sops-nix / agenix**, never commit plaintext.
|
`/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
|
- Data disk: plain `fileSystems."/mnt/data"` in configuration.nix — kept out of
|
||||||
disko so it is never formatted. Reference by `by-id` / `by-uuid`.
|
disko so it is never formatted. Reference by `by-id` / `by-uuid`.
|
||||||
- `system.stateVersion` = `26.05`, install-time schema. Do NOT bump on upgrades.
|
- `system.stateVersion` = `26.05`, install-time schema. Do NOT bump on upgrades.
|
||||||
|
|||||||
+7
-5
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
# sops-nix secret wiring (real host only; not imported by vm.nix).
|
# sops-nix secret wiring (real host only; not imported by vm.nix).
|
||||||
# Encrypted values live in ../secrets/jupiter.yaml, decrypted at activation to
|
# Encrypted values live in ../secrets/jupiter.yaml, decrypted at activation to
|
||||||
# /run/secrets/<name>. The host needs the age PRIVATE key at the keyFile path.
|
# /run/secrets/<name>.
|
||||||
#
|
#
|
||||||
# Deploy the private key with the install, e.g. nixos-anywhere:
|
# The host decrypts with its OWN SSH host key (age identity derived via
|
||||||
# --extra-files, placing your key at /var/lib/sops-nix/key.txt
|
# ssh-to-age, recipient listed in ../.sops.yaml). The key is pre-generated on
|
||||||
# or later derive a host age key from its ssh host key and add it to .sops.yaml.
|
# 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.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).
|
# Decrypts to /run/secrets/samba_password (root-only by default).
|
||||||
sops.secrets.samba_password = { };
|
sops.secrets.samba_password = { };
|
||||||
|
|||||||
+14
-5
@@ -3,13 +3,22 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlL0E3cWNIby95Q3RBcjlm
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKNTRmRDNFeitoYTU1djdV
|
||||||
QklPREM4cWRaelZEckFGUWpKUWExYmxrcWdrClByTVBlWDNlRGhMUkhEQzJONzFn
|
N2lwbGdXb21kSFhPS2xoYXNaaitmV0IzdFJvCjFoc29JcE1pNVkzUXFrRVltK0RI
|
||||||
alJVSzRMODN3U0cyRG93T3lHZkhXTWcKLS0tIFJ2V3I0V1NwakVlc2dCUW1iMHdR
|
MXFleXFRT1NmZ0taQ0lMaUxNNC9QbWMKLS0tIFZUOGpZUVRTYVVRa21Na01rR0ow
|
||||||
RlNsWnJJaDkvVnJnVlcyRXZmK1VkMUUKnv02m3vGjy8nTZV9ouItOEWmfZOABKDP
|
YXV3K3c5TVpUS3VMZFdmLzZvSUxlWFkKKU7BpZzLcAvWdMnJAHJS2gOLeS/JeU9e
|
||||||
VCvWcSy8R0NikkQ004XrBd1WhvyrUvqC6X9f4N9208BvmyDxEpKZbw==
|
1TCQgbYeFg0YHHa3Z/Vg8Q64hSyhpYYkE46dsjgSiZfCKbH4JaEPKA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2
|
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"
|
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]
|
mac: ENC[AES256_GCM,data:ynQU/B5RvwGw1ILJJexrZWtzNr7z3IUyy86v8q0KhRsMH2qSHF0BK0Pzq/DcT3vru+MwKat1hJfO21Pdu+HRsqtzUZRViTCbjtz+vmnjwyQvBqBPhEtxDeBoTgd5RqjXxzcuOADfv0NkfvU9p06x8YF8AVYq9ONvOEZiC4MamoY=,iv:1Wm4JP5Djyc9jdExMBcSjWw/zF1EXexjjMhePOEWCt8=,tag:A51vbK8fpb8sg+eCLwbWmg==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
Reference in New Issue
Block a user