aboutsummaryrefslogtreecommitdiff
path: root/modules/remote-unlock.nix
blob: fea9345ba708026fe7321a8b063aee1b50533d48 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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;
      };
    };
  };
}