nixos-config/lib/fudo/secrets.nix

185 lines
5.7 KiB
Nix

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.secrets;
encrypt-on-disk = { secret-name, target-host, target-pubkey, source-file }:
pkgs.stdenv.mkDerivation {
name = "${target-host}-${secret-name}-secret";
phases = "installPhase";
buildInputs = [ pkgs.age ];
installPhase = ''
age -a -r "${target-pubkey}" -o $out ${source-file}
'';
};
decrypt-script = { secret-name, source-file, target-host, target-file
, host-master-key, user, group, permissions }:
pkgs.writeShellScript
"decrypt-fudo-secret-${target-host}-${secret-name}.sh" ''
rm -rf ${target-file}
age -d -i ${host-master-key.key-path} -o ${target-file} ${
encrypt-on-disk {
inherit secret-name source-file target-host;
target-pubkey = host-master-key.public-key;
}
}
chown ${user}:${group} ${target-file}
chmod ${permissions} ${target-file}
'';
secret-service = target-host: secret-name:
{ source-file, target-file, user, group, permissions }: {
description = "decrypt secret ${secret-name} for ${target-host}.";
wantedBy = [ "default.target" ];
serviceConfig = {
Type = "oneshot";
ExecStartPre = pkgs.writeShellScript
"prepare-${target-host}-${secret-name}-secret-dir.sh" ''
TARGET_DIR=$(dirname ${target-file})
if [[ ! -d "$TARGET_DIR" ]]; then
mkdir -p "$TARGET_DIR"
fi
'';
ExecStart = let
host-master-key = config.fudo.hosts.${target-host}.master-key;
in decrypt-script {
inherit secret-name source-file target-host target-file host-master-key
user group permissions;
};
};
path = [ pkgs.age ];
};
secretOpts = { ... }: {
options = with types; {
source-file = mkOption {
type = path; # CAREFUL: this will copy the file to nixstore...keep on deploy host
description = "File from which to load the secret.";
};
target-file = mkOption {
type = str;
description =
"Target file on the host; the secret will be decrypted to this file.";
};
user = mkOption {
type = str;
description = "User (on target host) to which the file will belong.";
};
group = mkOption {
type = str;
description = "Group (on target host) to which the file will belong.";
default = "nogroup";
};
permissions = mkOption {
type = str;
description = "Permissions to set on the target file.";
default = "0400";
};
};
};
nix-build-users = let usernames = attrNames config.users.users;
in filter (user: (builtins.match "^nixbld[0-9]{1,2}$" user) != null)
usernames;
in {
options.fudo.secrets = with types; {
enable = mkOption {
type = bool;
description = "Include secrets in the build (disable when secrets are unavailable)";
default = true;
};
host-secrets = mkOption {
type = attrsOf (attrsOf (submodule secretOpts));
description = "Map of hosts to host secrets";
default = { };
};
host-deep-secrets = mkOption {
type = attrsOf (attrsOf (submodule secretOpts));
description = ''
Secrets that are only passed during deployment.
These secrets will be passed as nixops deployment secrets,
_unlike_ regular secrets that are passed to hosts as part of
the nixops store, but encrypted with the host SSH key. Regular
secrets are kept secret from normal users. These secrets will
be kept secret from _everybody_. However, they won't be
available on the host at boot until a new deployment occurs.
'';
default = { };
};
secret-users = mkOption {
type = listOf str;
description = "List of users with read-access to secrets.";
default = [ ];
};
secret-group = mkOption {
type = str;
description = "Group to which secrets will belong.";
default = "nixops-secrets";
};
secret-paths = mkOption {
type = listOf str;
description =
"Paths which contain (only) secrets. The contents will be reabable by the secret-group.";
default = [ ];
};
};
config = mkIf cfg.enable {
users.groups = {
${cfg.secret-group} = { members = cfg.secret-users ++ nix-build-users; };
};
systemd = let
hostname = config.instance.hostname;
host-secrets = if (hasAttr hostname cfg.host-secrets) then
cfg.host-secrets.${hostname}
else
{ };
host-secret-services = mapAttrs' (secret: secretOpts:
(nameValuePair "fudo-secret-${hostname}-${secret}"
(secret-service hostname secret secretOpts))) host-secrets;
in {
services = host-secret-services // {
fudo-secrets-watcher = {
wantedBy = [ "default.target" ];
description =
"Ensure access for group ${cfg.secret-group} to fudo secret paths.";
serviceConfig = {
ExecStart = pkgs.writeShellScript "fudo-secrets-watcher.sh"
(concatStringsSep "\n" (map (path: ''
chown -R root:${cfg.secret-group} ${path}
chmod -R u=rwX,g=rX,o= ${path}
'') cfg.secret-paths));
};
};
};
paths.fudo-secrets-watcher = mkIf ((length cfg.secret-paths) > 0) {
wantedBy = [ "default.target" ];
description = "Watch fudo secret paths, and correct perms on changes.";
pathConfig = {
PathChanged = cfg.secret-paths;
Unit = "fudo-secrets-watcher.service";
};
};
tmpfiles.rules = map (path: "d '${path}' - root ${cfg.secret-group} - -")
cfg.secret-paths;
};
};
}