feat: per-host darman passwords via sops; mercury sops; AdGuard pw via UI

- per-host darman_password (distinct hash) in secrets/{jupiter,vps,mercury}.yaml
  -> hashedPasswordFile; different console password per host (ssh still key-only)
- mercury: dedicated age key (on boot partition post-flash), sops-nix wired
- AdGuard: module has no secret hook + writable config -> mutableSettings=true,
  admin password set via web setup on first boot (never in repo/store)
This commit is contained in:
erik
2026-07-13 20:52:06 +02:00
parent b989490333
commit 1937d59b2c
8 changed files with 78 additions and 24 deletions
+6
View File
@@ -12,6 +12,9 @@ keys:
# (ssh-to-age). The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key. # (ssh-to-age). The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key.
- &jupiter age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt - &jupiter age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
- &vps age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp - &vps age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp
# mercury (rpi) uses a dedicated age key (SD image, no ssh-host-key delivery);
# the private key is dropped on its boot partition after flashing.
- &mercury age1cpty7zrgnn6l97upq00w5wa8zcvnkxkdt2jvhlj97jh83exure4slha43t
# Each host's secrets go to admin (edit) + that host only. Order matters: # Each host's secrets go to admin (edit) + that host only. Order matters:
# the first matching rule wins, so keep these specific paths. # the first matching rule wins, so keep these specific paths.
creation_rules: creation_rules:
@@ -21,3 +24,6 @@ creation_rules:
- path_regex: secrets/vps\.yaml$ - path_regex: secrets/vps\.yaml$
key_groups: key_groups:
- age: [ *admin, *vps ] - age: [ *admin, *vps ]
- path_regex: secrets/mercury\.yaml$
key_groups:
- age: [ *admin, *mercury ]
+1
View File
@@ -55,6 +55,7 @@
specialArgs = { inherit inputs; }; specialArgs = { inherit inputs; };
modules = [ modules = [
(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix") (nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix")
sops-nix.nixosModules.sops
./hosts/mercury/configuration.nix ./hosts/mercury/configuration.nix
]; ];
}; };
+1
View File
@@ -6,6 +6,7 @@
{ {
imports = [ imports = [
../../common.nix # shared base: user / ssh / nix / firewall ../../common.nix # shared base: user / ssh / nix / firewall
./secrets.nix # sops-nix: darman password (age key on boot part.)
../../services/unbound.nix # local recursive resolver (127.0.0.1:5335) ../../services/unbound.nix # local recursive resolver (127.0.0.1:5335)
../../services/adguardhome.nix # DNS adblock + DHCP, forwards to unbound ../../services/adguardhome.nix # DNS adblock + DHCP, forwards to unbound
]; ];
+17
View File
@@ -0,0 +1,17 @@
{ config, ... }:
# sops-nix wiring for mercury. Encrypted values in ../../secrets/mercury.yaml.
#
# SD images have no `--extra-files` step, so mercury uses a DEDICATED age key
# (not ssh-host-key-derived) placed on the FAT boot partition after flashing:
# mount the boot partition, copy ~/.config/homelab/mercury/age.txt to it as
# sops-age.txt, unmount. The key never enters the repo or the nix store.
{
sops.defaultSopsFile = ../../secrets/mercury.yaml;
sops.age.keyFile = "/boot/firmware/sops-age.txt";
# darman's console password (SSH is key-only regardless). Different hash per
# host = different password per host.
sops.secrets.darman_password.neededForUsers = true;
users.users.darman.hashedPasswordFile = config.sops.secrets.darman_password.path;
}
+4
View File
@@ -9,4 +9,8 @@
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.secrets.tailscale_authkey = { }; sops.secrets.tailscale_authkey = { };
# darman's console password (own hash = distinct from jupiter/mercury).
sops.secrets.darman_password.neededForUsers = true;
users.users.darman.hashedPasswordFile = config.sops.secrets.darman_password.path;
} }
+25
View File
@@ -0,0 +1,25 @@
darman_password: ENC[AES256_GCM,data:HzFkwGIyQL1sbPegJq970n3VASkaO/iY1t3fjodqk6FSoGX+4zlVoZfNAuTeiyAwv+0bZHhJoK+cAPKp5HMqsnhXJrhJUV7KVA3beyBXPPg8IpXi4X4maKv9KLIiWi2BJQuE421LUjb/2A==,iv:dcCD3aaEfVfPBh0jwFZkmNz0L/ZfRzMROJIHXd6pkG8=,tag:5vRhPo+fE28HrZPp6ZJ1MQ==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHMXdtQ2E3SENhY2RKWGx1
bWVPUUd6K2gwbnRjbjVZZnIvSkJpaW1kNXhNCmF5KzFZdDZoVjcybTIzeEltNk1X
QXg4bFJmQkhsUkVpQ1pEVHVUMmN2OWcKLS0tIExKdGwzWTJDbEd4b3V6SlIvSXo0
SDZMTXYyZThQamN1YUt2ODRhdm1kdEEK8wZbc4NCgWYJ4tZtq2UxATleAhJxor5b
H0Fjcz+V0CesaF1sxR1M1gUe7uXrhdNdNgZvr+aF/+jsKHq+thbelw==
-----END AGE ENCRYPTED FILE-----
recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNTJkd29HTWdFc1ZhTDFN
d2paTy91aGNNSlBWUDdqY255MzN1QUlibldJCmdHY0FVMU1uV2lRUGp0MjVFYjVO
RWNsMk0wU2oxaXdlS25TSHJIQnhxMXMKLS0tIG1GRjVSMEgyY2RYZDRSTllVd3g5
UGFjMERZTHhuWGpFVUE4TVIyYlA4WmMKcinqjjAaR3hJRLJepZPxunQz9WBvmP6+
ZsBrZS0j0B3MhTI+6gUWB82xozgmnh95w+SXSSZZvoss8RuicE5ysA==
-----END AGE ENCRYPTED FILE-----
recipient: age1cpty7zrgnn6l97upq00w5wa8zcvnkxkdt2jvhlj97jh83exure4slha43t
lastmodified: "2026-07-13T18:50:12Z"
mac: ENC[AES256_GCM,data:8e1VHvwPzI97toTrkbT53ky8OV8Jepyn5yhEHq87wcDP4cywZTm6WbJsqJNgvYdwwxIgaLK7WyausaFfnqIBRFSxauhmTD7mjXKwGFAvSyeyFL3RYbdTc3+GInTE8gF8oBQ7OGIREW6pcDFswzhmcTKbJ01V+vJ2sgxnvifNe7E=,iv:pmLsvkPro4aw//UdldKErbYfVmlK+xb2/4CbN0I6gQQ=,tag:sy9wBkTYIdYrlJ8U4TdJIw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1
+14 -13
View File
@@ -1,25 +1,26 @@
tailscale_authkey: ENC[AES256_GCM,data:K9An79w/7ZHl3Pb+TYgH1fzFqEsuBjgMqt4O9lS9csY=,iv:I3SWgDQDiWV/ItUs3biU+EG6nzy446Rd2Cf74fJVpVM=,tag:kn3PXvGkcheKF+26jLK3MQ==,type:str] tailscale_authkey: ENC[AES256_GCM,data:CBMidfdilBn1HP3TAu9TCOpuwPnzVQ/3OGCsf0vhFd8=,iv:V4UdZBPyzH8NkiXV40CxLdnfkCv5oG6xEQbNSmpmrmI=,tag:h8R2AIaFXgPycMIP8NyHVA==,type:str]
darman_password: ENC[AES256_GCM,data:otLevSn6bXBrjCu7Dv9PN7Umwrb7CD1PhC1i+J8rp2mURMYVuwF/ZnrlPC7/RDZgINIRBOXOkG9lZo3UjuRyqrtvfUzIm1jGL51l1zvZ/07oNW6OUp4IZEQpoOmz1GuXBlx+yKRQeRDHYA==,iv:pnwp3uAsBoiJdF+QV27h0jztgFJKrIqFlmMT7JkcHoA=,tag:Zb9ziCGsLQt+TcJRCv8tWA==,type:str]
sops: sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZNVRnY1djOFFpQjEzYzdi YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUOVhzYzN4ekZtNHF2TGcr
ZHhjZWliL2xlK2t6MncxSnk3ZW8ybHFNZ0hNCk0wWHdBajlMQWtjU21QQkxSV1M0 VjRITjg4UldQRStoaHhUa0RPODBVUTNETjF3CkljbzRDNytBanlZOEtjcEFMYW1W
bzJhbFBSSmdPU2VTV1NyQmhmUnlJUUEKLS0tIHFTTGlIam9MdllGamc2ZVZIaEFL QThTK1ZGR1plWFVrTThYMk5sU0N3bUkKLS0tIHNYclVIWHhNeGk2QXozdkN3TGdr
ZkE4cDk0aTdEMGtxVGM3U2JCMFppalEKnyyQCErY7e5pIFoYS6yYq5puqXAUgClJ VWNxK3pYM1IxZW9XU2QwYkVEeEEzUGcK0ZvVWycAfDq4Cb4fVFm40mUTLLuqK6DU
U9VkM7G1MZV+Z91XTKs63fd2QyV8ITTK82BMpwH39UzdbbU+mqFvqw== S/QUwFEiVAqMixxhS0vXOlovFQI+lzLLtLBi7Evi7ZI/fmdNqvz0+Q==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2 recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXZER4Q1NFSVJNUFpMS2JQ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpbUtuMHNhblFwTHdjQjIz
dVZseHFzc2lWOXVJQmdIZ2xHeWVDMEYyTFJjCkdKckNQNTZ4Zm9hRCtMc2lMZmFF MjdtL3VsZE5zU21tVTE5NnlBWVRqRDZGblY4Ck1ieUFkRk9tM25NN0pyMmtBWTZJ
TTh3cVBMVWJGRGY3Z2ErWlBlQUZhL0EKLS0tIGxGZDIzZEhsSFQ5SmNZblk5eHpy clhpNnhIQzlwd1VqQ1dzaVBhYjJkVGcKLS0tIFh0b0hvVU9xNVZ6QkNHcCs3UU5C
ZWpPU1RNZHNIRUhJMXE0dFFtcTRzUmsKnKzMUXY8MKG2Ilgzg1IIr7hvXuvLvzCH SEhKZEdlVkRyTTNzYUkvZ0JWYS83QjAK+2Shx79sIV2lYzAsRegzCnhScn2i2f/+
EaWB6C+J7/jG3dXfVTfKlubE7x62XDkWMa8OYMFELke/7GU6XXmHWg== Wptkf76aP9UpjhgNkxzedRebQPB7ti+UiVqCvLVimtuHcsm/NJPcRg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp recipient: age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp
lastmodified: "2026-07-12T22:59:51Z" lastmodified: "2026-07-13T18:50:14Z"
mac: ENC[AES256_GCM,data:yS13N11tXN2orKAP2ufRNdJbEGkY+bfZiA/7PwSWtgipIlGvnCKbr6yVqMElfNA39Znu+i1sWtk3FxWjRJQ3rM+uRpEdzoseMnYiHkvK0t5hC1ElBxwUdv8v/1RV5/halTV+5G8/lAd48ejgtnLLCpSbw6nPKrDoxb8/Ma5oaj0=,iv:MejfOs5F7eAmwZPsrLUF+6KPxWgACKKbnn5skCisuBo=,tag:X4BI6zDQgqXnnmCVOl25/A==,type:str] mac: ENC[AES256_GCM,data:52kyoG1moXQ9rDT7NLbxJby5ZiUn0Gym3u4kuLTsXxlGIvots7FipAg98BN4LZrL+8sGYMy2k8EAFzSB3F0tU5fi0f+R0F7CtKhK4nKCC5Hg70odN7HePobDw9dfR6hExLcSpUJBlUmpU0Lw1DL3MLjEEcfgzOYHmmrBv9rI4KU=,iv:2bIH2i874H1vg+nBJhsxohSYsjpFTb1toRw8mK5UN40=,tag:V2uijc0BymsLg3vDsHob9w==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.13.1 version: 3.13.1
+10 -11
View File
@@ -2,24 +2,23 @@
# AdGuard Home — network DNS (adblock) + DHCP. # AdGuard Home — network DNS (adblock) + DHCP.
# Forwards to the local unbound recursive resolver (services/unbound.nix). # Forwards to the local unbound recursive resolver (services/unbound.nix).
# Config is nix-owned (mutableSettings = false): the web UI can view but not #
# persist changes — edit here and redeploy. EXCEPTION: DHCP static leases live # Password note: the AdGuard NixOS module has NO secret/passwordFile hook, and
# in AdGuard's separate leases.json, so add those in the UI (they persist). # its config must be writable at runtime — so a sops secret can't be injected
# cleanly. Instead `mutableSettings = true`: the `settings` below are merged
# into AdGuard's state on start, and the ADMIN PASSWORD is set once via the web
# setup wizard on first boot (stored only in the Pi's runtime state — never in
# the repo or nix store). Trade-off: UI edits to other fields also persist.
# DHCP static leases likewise live in AdGuard's leases.json (add via UI).
{ {
services.adguardhome = { services.adguardhome = {
enable = true; enable = true;
openFirewall = true; # opens the web + DNS ports openFirewall = true; # opens the web + DNS ports
mutableSettings = false; # AdGuardHome.yaml is authoritative from nix mutableSettings = true; # merge settings; keep UI-set password + leases
# allowDHCP is implied by settings.dhcp.enabled (grants NET_RAW/NET_BIND). # allowDHCP is implied by settings.dhcp.enabled (grants NET_RAW/NET_BIND).
settings = { settings = {
# Web UI on :3000. Admin login — replace with YOUR bcrypt hash: http.address = "0.0.0.0:3000"; # web UI; set the admin password here on first boot
# nix run nixpkgs#apacheHttpd -- htpasswd -B -n -b admin 'yourpassword'
# (take the part after "admin:"). For repo hygiene, move this to sops later.
http.address = "0.0.0.0:3000";
users = [
{ name = "admin"; password = "$2y$10$REPLACE-WITH-BCRYPT-HASH"; }
];
dns = { dns = {
bind_hosts = [ "0.0.0.0" ]; bind_hosts = [ "0.0.0.0" ];