{ config, pkgs, lib, ... }: with lib; let cfg = config.security.runtimeSecrets; # 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 { 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 # to iterate over the set. secretFiles = 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 "/run/secrets/${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 (/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 = [ ]; text = '' echo setting up secrets store... 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; }; }