maxwell/custom/modules/secrets-store.nix

142 lines
3.9 KiB
Nix
Raw Normal View History

2020-10-20 01:11:28 +02:00
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.security.runtimeSecrets;
# A recursive attrset of submodule
storeType = types.attrsOf (types.submodule
{ freeformType = storeType;
options = secretOptions;
});
2020-10-26 02:23:58 +01:00
# Like types.path but also must exists
validFile = with types; path // {
check = x: path.check x && builtins.pathExists x;
};
2020-10-20 01:11:28 +02:00
# Secret file definition
secretOptions =
{ user = mkOption
{ type = types.str;
default = "root";
description = "Owner of the secret";
};
group = mkOption
{ type = types.str;
default = "root";
description = "Group with access to the secret.";
};
mode = mkOption
{ type = types.str;
2020-10-26 00:49:01 +01:00
default = "0440";
2020-10-20 01:11:28 +02:00
description = "File permission (octal format)";
};
path = mkOption
2020-10-26 02:23:58 +01:00
{ type = types.nullOr validFile;
2020-10-20 01:11:28 +02:00
default = null;
apply = toString;
description = "File to include in the secret store";
};
};
2020-10-20 01:11:28 +02:00
# Turns a nested attrset into a list
# of (path, value) pairs. It recurs
# until `cond val` is false.
attrsToIndex = cond: set:
let recurse = path: set:
let index = name: value:
if isAttrs value && cond value
then recurse (path ++ [name]) value
else singleton { path = path ++ [name]; value = value; };
in concatLists (mapAttrsToList index set);
in recurse [] set;
isFile = v: isAttrs v && v.path != "";
# Secret files flattened to an index. This is needed
2020-10-20 01:11:28 +02:00
# to iterate over the set.
secretFiles =
filter (pair: isFile pair.value)
(attrsToIndex (v: !isFile v) cfg);
2020-10-20 01:11:28 +02:00
# Secrets with paths rewritten to the store location
storedSecrets = mapAttrsRecursiveCond (v: !isFile v)
(names: secret:
if isFile secret
2020-10-26 00:49:01 +01:00
then "/run/secrets/${concatStringsSep "-" names}"
2020-10-20 01:11:28 +02:00
else secret) cfg;
in {
options.security.runtimeSecrets = mkOption {
type = storeType;
default = { };
description = ''
Definitions of runtime secrets. This is a freeform attributes
set: it can contain arbitrarily nested sets of secrets.
Secrets are paths to be copied into the secrets store
(/run/secrets) with proper permission and owenership.
'';
};
options.security.buildSecrets = mkOption {
type = types.attrs;
default = { };
description = ''
Definitions of build secrets. This is a freeform attrset
to be merged with the secrets-store and intended to store
unsafe secrets. This will be copied into the world-readable
Nix store, only use at a last resort.
'';
};
options.secrets = mkOption {
type = types.attrs;
readOnly = true;
default = recursiveUpdate storedSecrets config.security.buildSecrets;
description = ''
The attrset used to access stored secrets from NixOS
configuration and modules.
'';
};
# The `users` activation script may need access to secrets
config.system.activationScripts.users.deps = [ "secrets-copy" ];
# Install secrets, first
config.system.activationScripts.secrets-copy = {
deps = [ ];
2020-10-20 01:11:28 +02:00
text =
''
echo setting up secrets store...
2020-10-20 01:11:28 +02:00
rm -rf /run/secrets
'' + concatMapStrings (pair:
let
name = "${concatStringsSep "-" pair.path}";
secret = pair.value;
in
''
# Install secret ${name}
install -m ${secret.mode} -D ${secret.path} /run/secrets/${name}
'') secretFiles;
};
# Set secrets ownership, later because the
# `user` activation script hasn't run yet.
config.system.activationScripts.secrets-own = {
deps = [ "secrets-copy" "users" ];
text = concatMapStrings (pair:
let
name = "${concatStringsSep "-" pair.path}";
secret = pair.value;
in
''
echo setting secrets store ownership...
# Set ownership of ${name}
chown ${secret.user}:${secret.group} /run/secrets/${name}
'') secretFiles;
2020-10-20 01:11:28 +02:00
};
}