diff --git a/.sops.yaml b/.sops.yaml index f7862ad..f940a97 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -12,6 +12,9 @@ keys: # (ssh-to-age). The host decrypts at runtime using /etc/ssh/ssh_host_ed25519_key. - &jupiter age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt - &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: # the first matching rule wins, so keep these specific paths. creation_rules: @@ -21,3 +24,6 @@ creation_rules: - path_regex: secrets/vps\.yaml$ key_groups: - age: [ *admin, *vps ] + - path_regex: secrets/mercury\.yaml$ + key_groups: + - age: [ *admin, *mercury ] diff --git a/flake.nix b/flake.nix index a4c4f4c..3650041 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,7 @@ specialArgs = { inherit inputs; }; modules = [ (nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix") + sops-nix.nixosModules.sops ./hosts/mercury/configuration.nix ]; }; diff --git a/hosts/mercury/configuration.nix b/hosts/mercury/configuration.nix index 8ad9c5b..d6233e5 100644 --- a/hosts/mercury/configuration.nix +++ b/hosts/mercury/configuration.nix @@ -6,6 +6,7 @@ { imports = [ ../../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/adguardhome.nix # DNS adblock + DHCP, forwards to unbound ]; diff --git a/hosts/mercury/secrets.nix b/hosts/mercury/secrets.nix new file mode 100644 index 0000000..4e093d5 --- /dev/null +++ b/hosts/mercury/secrets.nix @@ -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; +} diff --git a/hosts/vps/secrets.nix b/hosts/vps/secrets.nix index d01f857..426de47 100644 --- a/hosts/vps/secrets.nix +++ b/hosts/vps/secrets.nix @@ -9,4 +9,8 @@ sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; 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; } diff --git a/secrets/mercury.yaml b/secrets/mercury.yaml new file mode 100644 index 0000000..d979fe0 --- /dev/null +++ b/secrets/mercury.yaml @@ -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 diff --git a/secrets/vps.yaml b/secrets/vps.yaml index 42d6745..1784ef7 100644 --- a/secrets/vps.yaml +++ b/secrets/vps.yaml @@ -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: age: - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZNVRnY1djOFFpQjEzYzdi - ZHhjZWliL2xlK2t6MncxSnk3ZW8ybHFNZ0hNCk0wWHdBajlMQWtjU21QQkxSV1M0 - bzJhbFBSSmdPU2VTV1NyQmhmUnlJUUEKLS0tIHFTTGlIam9MdllGamc2ZVZIaEFL - ZkE4cDk0aTdEMGtxVGM3U2JCMFppalEKnyyQCErY7e5pIFoYS6yYq5puqXAUgClJ - U9VkM7G1MZV+Z91XTKs63fd2QyV8ITTK82BMpwH39UzdbbU+mqFvqw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUOVhzYzN4ekZtNHF2TGcr + VjRITjg4UldQRStoaHhUa0RPODBVUTNETjF3CkljbzRDNytBanlZOEtjcEFMYW1W + QThTK1ZGR1plWFVrTThYMk5sU0N3bUkKLS0tIHNYclVIWHhNeGk2QXozdkN3TGdr + VWNxK3pYM1IxZW9XU2QwYkVEeEEzUGcK0ZvVWycAfDq4Cb4fVFm40mUTLLuqK6DU + S/QUwFEiVAqMixxhS0vXOlovFQI+lzLLtLBi7Evi7ZI/fmdNqvz0+Q== -----END AGE ENCRYPTED FILE----- recipient: age1cekcqyf7073fsytcjxaa9dr9zwkmn4vjg36rv2tgxdglzfv4jvxqvcj6z2 - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXZER4Q1NFSVJNUFpMS2JQ - dVZseHFzc2lWOXVJQmdIZ2xHeWVDMEYyTFJjCkdKckNQNTZ4Zm9hRCtMc2lMZmFF - TTh3cVBMVWJGRGY3Z2ErWlBlQUZhL0EKLS0tIGxGZDIzZEhsSFQ5SmNZblk5eHpy - ZWpPU1RNZHNIRUhJMXE0dFFtcTRzUmsKnKzMUXY8MKG2Ilgzg1IIr7hvXuvLvzCH - EaWB6C+J7/jG3dXfVTfKlubE7x62XDkWMa8OYMFELke/7GU6XXmHWg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpbUtuMHNhblFwTHdjQjIz + MjdtL3VsZE5zU21tVTE5NnlBWVRqRDZGblY4Ck1ieUFkRk9tM25NN0pyMmtBWTZJ + clhpNnhIQzlwd1VqQ1dzaVBhYjJkVGcKLS0tIFh0b0hvVU9xNVZ6QkNHcCs3UU5C + SEhKZEdlVkRyTTNzYUkvZ0JWYS83QjAK+2Shx79sIV2lYzAsRegzCnhScn2i2f/+ + Wptkf76aP9UpjhgNkxzedRebQPB7ti+UiVqCvLVimtuHcsm/NJPcRg== -----END AGE ENCRYPTED FILE----- recipient: age1hp72xyx2cnd05937e4eww95g5kdtn0wsf9j2nypw330pa69gfdxqn0lpkp - lastmodified: "2026-07-12T22:59:51Z" - mac: ENC[AES256_GCM,data:yS13N11tXN2orKAP2ufRNdJbEGkY+bfZiA/7PwSWtgipIlGvnCKbr6yVqMElfNA39Znu+i1sWtk3FxWjRJQ3rM+uRpEdzoseMnYiHkvK0t5hC1ElBxwUdv8v/1RV5/halTV+5G8/lAd48ejgtnLLCpSbw6nPKrDoxb8/Ma5oaj0=,iv:MejfOs5F7eAmwZPsrLUF+6KPxWgACKKbnn5skCisuBo=,tag:X4BI6zDQgqXnnmCVOl25/A==,type:str] + lastmodified: "2026-07-13T18:50:14Z" + mac: ENC[AES256_GCM,data:52kyoG1moXQ9rDT7NLbxJby5ZiUn0Gym3u4kuLTsXxlGIvots7FipAg98BN4LZrL+8sGYMy2k8EAFzSB3F0tU5fi0f+R0F7CtKhK4nKCC5Hg70odN7HePobDw9dfR6hExLcSpUJBlUmpU0Lw1DL3MLjEEcfgzOYHmmrBv9rI4KU=,iv:2bIH2i874H1vg+nBJhsxohSYsjpFTb1toRw8mK5UN40=,tag:V2uijc0BymsLg3vDsHob9w==,type:str] unencrypted_suffix: _unencrypted version: 3.13.1 diff --git a/services/adguardhome.nix b/services/adguardhome.nix index 8628a20..e3f9db6 100644 --- a/services/adguardhome.nix +++ b/services/adguardhome.nix @@ -2,24 +2,23 @@ # AdGuard Home — network DNS (adblock) + DHCP. # 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 -# in AdGuard's separate leases.json, so add those in the UI (they persist). +# +# Password note: the AdGuard NixOS module has NO secret/passwordFile hook, and +# 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 = { enable = true; 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). settings = { - # Web UI on :3000. Admin login — replace with YOUR bcrypt hash: - # 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"; } - ]; + http.address = "0.0.0.0:3000"; # web UI; set the admin password here on first boot dns = { bind_hosts = [ "0.0.0.0" ];