From 28b336919d3f133d3c2704b58e05d57e0e59363b Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Thu, 25 Feb 2021 02:23:15 -0800 Subject: [PATCH] modules/age: symlink files into place This follows sops-nix's implementation, where it creates a `/run/secrets.d` ramfs mountpoint and a "generation" each time the activation script runs, and then symlinks `/run/secrets` to `/run/secrets.d/[generation]`. --- modules/age.nix | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/modules/age.nix b/modules/age.nix index a2a4803..d85b871 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -11,13 +11,15 @@ let identities = builtins.concatStringsSep " " (map (path: "-i ${path}") cfg.sshKeyPaths); installSecret = secretType: '' - echo "decrypting ${secretType.file} to ${secretType.path}..." - TMP_FILE="${secretType.path}.tmp" - mkdir -p $(dirname ${secretType.path}) + echo "decrypting '${secretType.file}' to '${cfg.secretsMountPoint}/$_count/${secretType.name}'..." + TMP_FILE="${cfg.secretsMountPoint}/$_count/${secretType.name}.tmp" + mkdir -p "$(dirname "${cfg.secretsMountPoint}/$_count/${secretType.name}")" + mkdir -p "$(dirname "${secretType.path}")" (umask 0400; LANG=${config.i18n.defaultLocale} ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secretType.file}") chmod ${secretType.mode} "$TMP_FILE" chown ${secretType.owner}:${secretType.group} "$TMP_FILE" - mv -f "$TMP_FILE" '${secretType.path}' + mv -f "$TMP_FILE" "${cfg.secretsMountPoint}/$_count/${secretType.name}" + [ "${secretType.path}" != "/run/secrets/${secretType.name}" ] && ln -sfn "/run/secrets/${secretType.name}" "${secretType.path}" ''; rootOwnedSecrets = builtins.filter (st: st.owner == "root" && st.group == "root") (builtins.attrValues cfg.secrets); @@ -80,6 +82,17 @@ in { Attrset of secrets. ''; }; + secretsMountPoint = mkOption { + type = types.addCheck types.str + (s: + (builtins.match "[ \t\n]*" s) == null # non-empty + && (builtins.match ".+/" s) == null) # without trailing slash + // { description = "${types.str.description} (with check: non-empty without trailing slash)"; }; + default = "/run/secrets.d"; + description = '' + Where secrets are created before they are symlinked to /run/secrets + ''; + }; sshKeyPaths = mkOption { type = types.listOf types.path; default = if config.services.openssh.enable then @@ -96,9 +109,26 @@ in { message = "age.sshKeyPaths must be set."; }]; + # Create a new directory full of secrets for symlinking (this helps + # ensure removed secrets are actually removed, or at least become + # invalid symlinks). + system.activationScripts.agenixMountSecrets = '' + _count="$(basename "$(readlink /run/secrets)" || echo 0)" + (( ++_count )) + echo "[agenix] symlinking new secrets generation $_count to /run/secrets..." + mkdir -pm 0750 "${cfg.secretsMountPoint}" + mount | grep "${cfg.secretsMountPoint} type ramfs" -q || mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0750 + mkdir -pm 0750 "${cfg.secretsMountPoint}/$_count" + chown :keys "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_count" + ln -sfn "${cfg.secretsMountPoint}/$_count" /run/secrets + ''; + # Secrets with root owner and group can be installed before users # exist. This allows user password files to be encrypted. - system.activationScripts.agenixRoot = installRootOwnedSecrets; + system.activationScripts.agenixRoot = { + text = installRootOwnedSecrets; + deps = [ "agenixMountSecrets" ]; + }; system.activationScripts.users.deps = [ "agenixRoot" ]; # Other secrets need to wait for users and groups to exist.