From 2b61601dd95244e31d82613621955effb91f7222 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Fri, 28 Nov 2025 14:05:44 -0800 Subject: add a module to remotely unlock machines For machines with full disk encryption, we can remotely unlock them from bree. A systemd timer will run every 10 minutes and check if we need to unlock the host. If we need to, it will SSH and provide the passphrase to unlock the disk(s). --- machines/nixos/x86_64-linux/bree.nix | 12 ++++ modules/default.nix | 1 + modules/remote-unlock.nix | 111 +++++++++++++++++++++++++++++++++++ profiles/remote-unlock.nix | 3 + secrets/bree/disk-passphrase.age | 8 +++ secrets/bree/disk-unlock-key.age | Bin 0 -> 721 bytes secrets/secrets.nix | 10 ++++ 7 files changed, 145 insertions(+) create mode 100644 modules/remote-unlock.nix create mode 100644 secrets/bree/disk-passphrase.age create mode 100644 secrets/bree/disk-unlock-key.age diff --git a/machines/nixos/x86_64-linux/bree.nix b/machines/nixos/x86_64-linux/bree.nix index 2f564b5..f91bf4f 100644 --- a/machines/nixos/x86_64-linux/bree.nix +++ b/machines/nixos/x86_64-linux/bree.nix @@ -27,5 +27,17 @@ }; }; + age.secrets.disk-unlock-key.file = ../../../secrets/bree/disk-unlock-key.age; + age.secrets.disk-passphrase.file = ../../../secrets/bree/disk-passphrase.age; + + services.remoteDiskUnlock = { + enable = true; + hosts = [ + "192.168.1.114" + ]; + sshKeyPath = config.age.secrets.disk-unlock-key.path; + passphrasePath = config.age.secrets.disk-passphrase.path; + }; + system.stateVersion = "23.11"; # Did you read the comment? } diff --git a/modules/default.nix b/modules/default.nix index d6d7b65..f936646 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -3,6 +3,7 @@ imports = [ ./home-manager.nix ./host-config.nix + ./remote-unlock.nix ./ssh.nix ./user.nix ]; diff --git a/modules/remote-unlock.nix b/modules/remote-unlock.nix new file mode 100644 index 0000000..fea9345 --- /dev/null +++ b/modules/remote-unlock.nix @@ -0,0 +1,111 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; + +let + cfg = config.services.remoteDiskUnlock; + + unlockScript = pkgs.writeShellScript "remote-disk-unlock" '' + #!/usr/bin/env bash + set -euo pipefail + + SSH_KEY="$CREDENTIALS_DIRECTORY/ssh-key" + PASSPHRASE_FILE="$CREDENTIALS_DIRECTORY/passphrase" + + for server in ${concatStringsSep " " cfg.hosts}; do + echo "Probing host $server on port 22" + if ${pkgs.netcat}/bin/nc -z -w 5 "$server" 22 2>/dev/null; then + echo "Host $server is already unlocked, skipping" + continue + fi + echo "No response on port 22, probing host $server on port 911" + if ${pkgs.netcat}/bin/nc -z -w 5 "$server" 911 2>/dev/null; then + echo "Host $server is waiting for unlock - unlocking" + ${pkgs.openssh}/bin/ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null \ + -i "$SSH_KEY" -p 911 "root@$server" < "$PASSPHRASE_FILE" || true + else + echo "Host $server is down, retry later" + fi + done + ''; +in +{ + options.services.remoteDiskUnlock = { + enable = mkEnableOption "remote disk unlock service"; + + hosts = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of hostnames/IPs to monitor and unlock"; + example = [ + "server1.local" + "192.168.1.100" + ]; + }; + + sshKeyPath = mkOption { + type = types.path; + description = "Path to SSH private key"; + example = "/run/agenix/disk-unlock-key"; + }; + + passphrasePath = mkOption { + type = types.path; + description = "Path to disk passphrase file"; + example = "/run/agenix/disk-passphrase"; + }; + + interval = mkOption { + type = types.str; + default = "10min"; + description = "How often to check hosts (systemd timer format)"; + }; + }; + + config = mkIf cfg.enable { + systemd.services.remote-disk-unlock = { + description = "Unlock remote encrypted disks"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${unlockScript}"; + DynamicUser = true; + LoadCredential = [ + "ssh-key:${cfg.sshKeyPath}" + "passphrase:${cfg.passphrasePath}" + ]; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + }; + + systemd.timers.remote-disk-unlock = { + description = "Check and unlock remote disks periodically"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "1min"; + OnUnitActiveSec = cfg.interval; + Persistent = false; + }; + }; + }; +} diff --git a/profiles/remote-unlock.nix b/profiles/remote-unlock.nix index b0e3fe8..ea211ad 100644 --- a/profiles/remote-unlock.nix +++ b/profiles/remote-unlock.nix @@ -15,7 +15,10 @@ "/etc/initrd/ssh_host_ed25519_key" ]; authorizedKeys = [ + # my personal key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINBkozy+X96u5ciX766bJ/AyQ3xm1tXZTIr5+4PVFZFi" + # key used to automatically unlock + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPr9Dv2MjZoRltmxi21PoS/42KnOhYxuq9r6ER62vjAx" ]; }; }; diff --git a/secrets/bree/disk-passphrase.age b/secrets/bree/disk-passphrase.age new file mode 100644 index 0000000..3811173 --- /dev/null +++ b/secrets/bree/disk-passphrase.age @@ -0,0 +1,8 @@ +age-encryption.org/v1 +-> ssh-ed25519 pFjJaA r/Q4nB/VcKaVXoJjDuIgnMVUr5K0rhrsVVq2lvQgQRQ +ZmwHs0sWxVKjS9njqPQR4rEV1aXxS80wWJQrAuf47vM +-> ssh-ed25519 OxmK1A /9e7fHg/Nh929cY7+0EagkxwME4jo0RxzBwdh8tuZnM +9UPI8Vnwebjick9WPlcT8lvNub687qchX4D4ntbanos +--- bwBCnL9gJhzuygCddmh0h0OXh/C6ysAgMfH9QBrQUMY + +I4ڍ:;X3T.n{A0^笆4F]P.uΕެ \ No newline at end of file diff --git a/secrets/bree/disk-unlock-key.age b/secrets/bree/disk-unlock-key.age new file mode 100644 index 0000000..6d9a549 Binary files /dev/null and b/secrets/bree/disk-unlock-key.age differ diff --git a/secrets/secrets.nix b/secrets/secrets.nix index a8f01cf..155a88b 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -67,6 +67,16 @@ in hosts.bree ]; + "bree/disk-passphrase.age".publicKeys = [ + users.fcuny + hosts.bree + ]; + + "bree/disk-unlock-key.age".publicKeys = [ + users.fcuny + hosts.bree + ]; + "rivendell/wireguard.age".publicKeys = [ users.fcuny hosts.rivendell -- cgit v1.2.3