{ 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 { 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 "/var/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 (/var/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 = '' secret=${(head secretFiles).value.path} if test -f "$secret"; then echo copying secrets... rm -rf /var/secrets ${concatMapStrings (f: '' install -m ${f.value.mode} -D ${f.value.path} /var/secrets/${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} /var/secrets/${f.name} '') secretFiles} ''; }; }