nixos-config/lib/fudo/secrets.nix

222 lines
6.8 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 -f ${target-file}
touch ${target-file}
chown ${user}:${group} ${target-file}
chmod ${permissions} ${target-file}
# NOTE: silly hack because sometimes age leaves a blank line
# Only include lines with at least one non-space character
SRC=$(mktemp fudo-secret-${target-host}-${secret-name}.XXXXXXXX)
cat ${encrypt-on-disk {
inherit secret-name source-file target-host;
target-pubkey = host-master-key.public-key;
}} | grep "[^ ]" > $SRC
age -d -i ${host-master-key.key-path} -o ${target-file} $SRC
rm -f $SRC
'';
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";
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 = { name, ... }: {
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. If unspecified, a random new password will be generated.";
default = "${generate-secret name}/passwd";
};
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";
};
metadata = mkOption {
type = attrsOf anything;
description = "Arbitrary metadata associated with this secret.";
default = {};
};
};
};
nix-build-users = let usernames = attrNames config.users.users;
in filter (user: (builtins.match "^nixbld[0-9]{1,2}$" user) != null)
usernames;
generate-secret = name: pkgs.stdenv.mkDerivation {
name = "${name}-generated-passwd";
phases = [ "installPhase" ];
buildInputs = with pkgs; [ pwgen ];
buildPhase = ''
echo "${name}-${config.instance.build-timestamp}" >> file.txt
pwgen --secure --symbols --num-passwords=1 --sha1=file.txt 40 > passwd
rm -f file.txt
'';
installPhase = ''
mkdir $out
mv passwd $out/passwd
'';
};
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;
trace-all = obj: builtins.trace obj obj;
host-secret-paths = mapAttrsToList
(secret: secretOpts:
let perms = if secretOpts.group != "nobody" then "550" else "500";
in "d ${dirOf secretOpts.target-file} ${perms} ${secretOpts.user} ${secretOpts.group} - -")
host-secrets;
build-secret-paths =
map (path: "d '${path}' - root ${cfg.secret-group} - -")
cfg.secret-paths;
in {
tmpfiles.rules = host-secret-paths ++ build-secret-paths;
services = host-secret-services // {
fudo-secrets-watcher = mkIf (length cfg.secret-paths > 0) {
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";
};
};
};
};
}