Avoids adding an extra flake input only to fetch a single module and package. Reviewed-by: Aleix Boné <abonerib@bsc.es> Tested-by: Rodrigo Arias Mallo <rodrigo.arias@bsc.es>
		
			
				
	
	
		
			358 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
| {
 | |
|   config,
 | |
|   options,
 | |
|   lib,
 | |
|   pkgs,
 | |
|   ...
 | |
| }:
 | |
| with lib;
 | |
| let
 | |
|   cfg = config.age;
 | |
| 
 | |
|   isDarwin = lib.attrsets.hasAttrByPath [ "environment" "darwinConfig" ] options;
 | |
| 
 | |
|   ageBin = config.age.ageBin;
 | |
| 
 | |
|   users = config.users.users;
 | |
| 
 | |
|   sysusersEnabled =
 | |
|     if isDarwin then
 | |
|       false
 | |
|     else
 | |
|       options.systemd ? sysusers && (config.systemd.sysusers.enable || config.services.userborn.enable);
 | |
| 
 | |
|   mountCommand =
 | |
|     if isDarwin then
 | |
|       ''
 | |
|         if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then
 | |
|             num_sectors=1048576
 | |
|             dev=$(hdiutil attach -nomount ram://"$num_sectors" | sed 's/[[:space:]]*$//')
 | |
|             newfs_hfs -v agenix "$dev"
 | |
|             mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}"
 | |
|         fi
 | |
|       ''
 | |
|     else
 | |
|       ''
 | |
|         grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts ||
 | |
|           mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751
 | |
|       '';
 | |
|   newGeneration = ''
 | |
|     _agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
 | |
|     (( ++_agenix_generation ))
 | |
|     echo "[agenix] creating new generation in ${cfg.secretsMountPoint}/$_agenix_generation"
 | |
|     mkdir -p "${cfg.secretsMountPoint}"
 | |
|     chmod 0751 "${cfg.secretsMountPoint}"
 | |
|     ${mountCommand}
 | |
|     mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation"
 | |
|     chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
 | |
|   '';
 | |
| 
 | |
|   chownGroup = if isDarwin then "admin" else "keys";
 | |
|   # chown the secrets mountpoint and the current generation to the keys group
 | |
|   # instead of leaving it root:root.
 | |
|   chownMountPoint = ''
 | |
|     chown :${chownGroup} "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation"
 | |
|   '';
 | |
| 
 | |
|   setTruePath = secretType: ''
 | |
|     ${
 | |
|       if secretType.symlink then
 | |
|         ''
 | |
|           _truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
 | |
|         ''
 | |
|       else
 | |
|         ''
 | |
|           _truePath="${secretType.path}"
 | |
|         ''
 | |
|     }
 | |
|   '';
 | |
| 
 | |
|   installSecret = secretType: ''
 | |
|     ${setTruePath secretType}
 | |
|     echo "decrypting '${secretType.file}' to '$_truePath'..."
 | |
|     TMP_FILE="$_truePath.tmp"
 | |
| 
 | |
|     IDENTITIES=()
 | |
|     for identity in ${toString cfg.identityPaths}; do
 | |
|       test -r "$identity" || continue
 | |
|       test -s "$identity" || continue
 | |
|       IDENTITIES+=(-i)
 | |
|       IDENTITIES+=("$identity")
 | |
|     done
 | |
| 
 | |
|     test "''${#IDENTITIES[@]}" -eq 0 && echo "[agenix] WARNING: no readable identities found!"
 | |
| 
 | |
|     mkdir -p "$(dirname "$_truePath")"
 | |
|     [ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && mkdir -p "$(dirname "${secretType.path}")"
 | |
|     (
 | |
|       umask u=r,g=,o=
 | |
|       test -f "${secretType.file}" || echo '[agenix] WARNING: encrypted file ${secretType.file} does not exist!'
 | |
|       test -d "$(dirname "$TMP_FILE")" || echo "[agenix] WARNING: $(dirname "$TMP_FILE") does not exist!"
 | |
|       LANG=${
 | |
|         config.i18n.defaultLocale or "C"
 | |
|       } ${ageBin} --decrypt "''${IDENTITIES[@]}" -o "$TMP_FILE" "${secretType.file}"
 | |
|     )
 | |
|     chmod ${secretType.mode} "$TMP_FILE"
 | |
|     mv -f "$TMP_FILE" "$_truePath"
 | |
| 
 | |
|     ${optionalString secretType.symlink ''
 | |
|       [ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfT "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
 | |
|     ''}
 | |
|   '';
 | |
| 
 | |
|   testIdentities = map (path: ''
 | |
|     test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
 | |
|   '') cfg.identityPaths;
 | |
| 
 | |
|   cleanupAndLink = ''
 | |
|     _agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
 | |
|     (( ++_agenix_generation ))
 | |
|     echo "[agenix] symlinking new secrets to ${cfg.secretsDir} (generation $_agenix_generation)..."
 | |
|     ln -sfT "${cfg.secretsMountPoint}/$_agenix_generation" ${cfg.secretsDir}
 | |
| 
 | |
|     (( _agenix_generation > 1 )) && {
 | |
|     echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
 | |
|     rm -rf "${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))"
 | |
|     }
 | |
|   '';
 | |
| 
 | |
|   installSecrets = builtins.concatStringsSep "\n" (
 | |
|     [ "echo '[agenix] decrypting secrets...'" ]
 | |
|     ++ testIdentities
 | |
|     ++ (map installSecret (builtins.attrValues cfg.secrets))
 | |
|     ++ [ cleanupAndLink ]
 | |
|   );
 | |
| 
 | |
|   chownSecret = secretType: ''
 | |
|     ${setTruePath secretType}
 | |
|     chown ${secretType.owner}:${secretType.group} "$_truePath"
 | |
|   '';
 | |
| 
 | |
|   chownSecrets = builtins.concatStringsSep "\n" (
 | |
|     [ "echo '[agenix] chowning...'" ]
 | |
|     ++ [ chownMountPoint ]
 | |
|     ++ (map chownSecret (builtins.attrValues cfg.secrets))
 | |
|   );
 | |
| 
 | |
|   secretType = types.submodule (
 | |
|     { config, ... }:
 | |
|     {
 | |
|       options = {
 | |
|         name = mkOption {
 | |
|           type = types.str;
 | |
|           default = config._module.args.name;
 | |
|           defaultText = literalExpression "config._module.args.name";
 | |
|           description = ''
 | |
|             Name of the file used in {option}`age.secretsDir`
 | |
|           '';
 | |
|         };
 | |
|         file = mkOption {
 | |
|           type = types.path;
 | |
|           description = ''
 | |
|             Age file the secret is loaded from.
 | |
|           '';
 | |
|         };
 | |
|         path = mkOption {
 | |
|           type = types.str;
 | |
|           default = "${cfg.secretsDir}/${config.name}";
 | |
|           defaultText = literalExpression ''
 | |
|             "''${cfg.secretsDir}/''${config.name}"
 | |
|           '';
 | |
|           description = ''
 | |
|             Path where the decrypted secret is installed.
 | |
|           '';
 | |
|         };
 | |
|         mode = mkOption {
 | |
|           type = types.str;
 | |
|           default = "0400";
 | |
|           description = ''
 | |
|             Permissions mode of the decrypted secret in a format understood by chmod.
 | |
|           '';
 | |
|         };
 | |
|         owner = mkOption {
 | |
|           type = types.str;
 | |
|           default = "0";
 | |
|           description = ''
 | |
|             User of the decrypted secret.
 | |
|           '';
 | |
|         };
 | |
|         group = mkOption {
 | |
|           type = types.str;
 | |
|           default = users.${config.owner}.group or "0";
 | |
|           defaultText = literalExpression ''
 | |
|             users.''${config.owner}.group or "0"
 | |
|           '';
 | |
|           description = ''
 | |
|             Group of the decrypted secret.
 | |
|           '';
 | |
|         };
 | |
|         symlink = mkEnableOption "symlinking secrets to their destination" // {
 | |
|           default = true;
 | |
|         };
 | |
|       };
 | |
|     }
 | |
|   );
 | |
| in
 | |
| {
 | |
|   imports = [
 | |
|     (mkRenamedOptionModule [ "age" "sshKeyPaths" ] [ "age" "identityPaths" ])
 | |
|   ];
 | |
| 
 | |
|   options.age = {
 | |
|     ageBin = mkOption {
 | |
|       type = types.str;
 | |
|       default = "${pkgs.age}/bin/age";
 | |
|       defaultText = literalExpression ''
 | |
|         "''${pkgs.age}/bin/age"
 | |
|       '';
 | |
|       description = ''
 | |
|         The age executable to use.
 | |
|       '';
 | |
|     };
 | |
|     secrets = mkOption {
 | |
|       type = types.attrsOf secretType;
 | |
|       default = { };
 | |
|       description = ''
 | |
|         Attrset of secrets.
 | |
|       '';
 | |
|     };
 | |
|     secretsDir = mkOption {
 | |
|       type = types.path;
 | |
|       default = "/run/agenix";
 | |
|       description = ''
 | |
|         Folder where secrets are symlinked to
 | |
|       '';
 | |
|     };
 | |
|     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/agenix.d";
 | |
|       description = ''
 | |
|         Where secrets are created before they are symlinked to {option}`age.secretsDir`
 | |
|       '';
 | |
|     };
 | |
|     identityPaths = mkOption {
 | |
|       type = types.listOf types.path;
 | |
|       default =
 | |
|         if isDarwin then
 | |
|           [
 | |
|             "/etc/ssh/ssh_host_ed25519_key"
 | |
|             "/etc/ssh/ssh_host_rsa_key"
 | |
|           ]
 | |
|         else if (config.services.openssh.enable or false) then
 | |
|           map (e: e.path) (
 | |
|             lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys
 | |
|           )
 | |
|         else
 | |
|           [ ];
 | |
|       defaultText = literalExpression ''
 | |
|         if isDarwin
 | |
|         then [
 | |
|           "/etc/ssh/ssh_host_ed25519_key"
 | |
|           "/etc/ssh/ssh_host_rsa_key"
 | |
|         ]
 | |
|         else if (config.services.openssh.enable or false)
 | |
|         then map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)
 | |
|         else [];
 | |
|       '';
 | |
|       description = ''
 | |
|         Path to SSH keys to be used as identities in age decryption.
 | |
|       '';
 | |
|     };
 | |
|   };
 | |
| 
 | |
|   config = mkIf (cfg.secrets != { }) (mkMerge [
 | |
|     {
 | |
|       assertions = [
 | |
|         {
 | |
|           assertion = cfg.identityPaths != [ ];
 | |
|           message = "age.identityPaths must be set, for example by enabling openssh.";
 | |
|         }
 | |
|       ];
 | |
|     }
 | |
|     (optionalAttrs (!isDarwin) {
 | |
|       # When using sysusers we no longer be started as an activation script
 | |
|       # because those are started in initrd while sysusers is started later.
 | |
|       systemd.services.agenix-install-secrets = mkIf sysusersEnabled {
 | |
|         wantedBy = [ "sysinit.target" ];
 | |
|         after = [ "systemd-sysusers.service" ];
 | |
|         unitConfig.DefaultDependencies = "no";
 | |
| 
 | |
|         path = [ pkgs.mount ];
 | |
|         serviceConfig = {
 | |
|           Type = "oneshot";
 | |
|           ExecStart = pkgs.writeShellScript "agenix-install" (concatLines [
 | |
|             newGeneration
 | |
|             installSecrets
 | |
|             chownSecrets
 | |
|           ]);
 | |
|           RemainAfterExit = true;
 | |
|         };
 | |
|       };
 | |
| 
 | |
|       # 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 = mkIf (!sysusersEnabled) {
 | |
|         agenixNewGeneration = {
 | |
|           text = newGeneration;
 | |
|           deps = [
 | |
|             "specialfs"
 | |
|           ];
 | |
|         };
 | |
| 
 | |
|         agenixInstall = {
 | |
|           text = installSecrets;
 | |
|           deps = [
 | |
|             "agenixNewGeneration"
 | |
|             "specialfs"
 | |
|           ];
 | |
|         };
 | |
| 
 | |
|         # So user passwords can be encrypted.
 | |
|         users.deps = [ "agenixInstall" ];
 | |
| 
 | |
|         # Change ownership and group after users and groups are made.
 | |
|         agenixChown = {
 | |
|           text = chownSecrets;
 | |
|           deps = [
 | |
|             "users"
 | |
|             "groups"
 | |
|           ];
 | |
|         };
 | |
| 
 | |
|         # So other activation scripts can depend on agenix being done.
 | |
|         agenix = {
 | |
|           text = "";
 | |
|           deps = [ "agenixChown" ];
 | |
|         };
 | |
|       };
 | |
|     })
 | |
| 
 | |
|     (optionalAttrs isDarwin {
 | |
|       launchd.daemons.activate-agenix = {
 | |
|         script = ''
 | |
|           set -e
 | |
|           set -o pipefail
 | |
|           export PATH="${pkgs.gnugrep}/bin:${pkgs.coreutils}/bin:@out@/sw/bin:/usr/bin:/bin:/usr/sbin:/sbin"
 | |
|           ${newGeneration}
 | |
|           ${installSecrets}
 | |
|           ${chownSecrets}
 | |
|           exit 0
 | |
|         '';
 | |
|         serviceConfig = {
 | |
|           RunAtLoad = true;
 | |
|           KeepAlive.SuccessfulExit = false;
 | |
|         };
 | |
|       };
 | |
|     })
 | |
|   ]);
 | |
| }
 |