140 lines
3.9 KiB
Nix
140 lines
3.9 KiB
Nix
{ config, pkgs, lib, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.security.runtimeSecrets;
|
|
|
|
secretsStore = "/var/secrets";
|
|
|
|
# A recursive attrset of submodule
|
|
storeType = types.attrsOf (types.submodule
|
|
{ freeformType = storeType;
|
|
options = secretOptions;
|
|
});
|
|
|
|
# Like types.path but also must exists
|
|
validFile = with types; path // {
|
|
check = x: path.check x && builtins.pathExists x;
|
|
};
|
|
|
|
# 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;
|
|
default = "0440";
|
|
description = "File permission (octal format)";
|
|
};
|
|
path = mkOption
|
|
{ type = types.nullOr validFile;
|
|
default = null;
|
|
apply = toString;
|
|
description = "File to include in the secret store";
|
|
};
|
|
};
|
|
|
|
# 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 { loc = 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
|
|
# to iterate over the set. It contains: {name, path, value}
|
|
secretFiles =
|
|
(map (x: x // { name = concatStringsSep "-" x.loc; })
|
|
(filter (pair: isFile pair.value)
|
|
(attrsToIndex (v: !isFile v) cfg)));
|
|
|
|
# Secrets with paths rewritten to the store location
|
|
storedSecrets = mapAttrsRecursiveCond (v: !isFile v)
|
|
(names: secret:
|
|
if isFile secret
|
|
then "${secretsStore}/${concatStringsSep "-" names}"
|
|
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
|
|
(${secretsStore}) with proper permission and ownership.
|
|
'';
|
|
};
|
|
|
|
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 = [ ];
|
|
text =
|
|
''
|
|
secret=${(head secretFiles).value.path}
|
|
if test -f "$secret"; then
|
|
echo copying secrets...
|
|
rm -rf ${secretsStore}
|
|
${concatMapStrings (f: ''
|
|
install -m ${f.value.mode} -D ${f.value.path} ${secretsStore}/${f.name}
|
|
'') secretFiles}
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# Set secrets ownership, later because the
|
|
# `user` activation script hasn't run yet.
|
|
config.system.activationScripts.secrets-own = {
|
|
deps = [ "users" "groups" ];
|
|
text =
|
|
''
|
|
echo setting secrets ownership...
|
|
${concatMapStrings (f: ''
|
|
chown ${f.value.user}:${f.value.group} ${secretsStore}/${f.name}
|
|
'') secretFiles}
|
|
'';
|
|
};
|
|
|
|
}
|