Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Templates #96

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 133 additions & 24 deletions modules/age.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,42 @@ let
users = config.users.users;

identities = builtins.concatStringsSep " " (map (path: "-i ${path}") cfg.identityPaths);
installSecret = secretType: ''
${if secretType.symlink then ''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
installSecret = secret: ''
${if secret.symlink then ''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secret.name}"
'' else ''
_truePath="${secretType.path}"
_truePath="${secret.path}"
''}
echo "decrypting '${secretType.file}' to '$_truePath'..."

${if secret ? file then ''
echo "decrypting '${secret.file}' to '$_truePath'..."
'' else ''
echo "constructing template ${secret.template} at '$_truePath'..."
''}

TMP_FILE="$_truePath.tmp"
mkdir -p "$(dirname "$_truePath")"
[ "${secretType.path}" != "/run/agenix/${secretType.name}" ] && mkdir -p "$(dirname "${secretType.path}")"
(
umask u=r,g=,o=
LANG=${config.i18n.defaultLocale} ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secretType.file}"
)
chmod ${secretType.mode} "$TMP_FILE"
chown ${secretType.owner}:${secretType.group} "$TMP_FILE"
[ "${secret.path}" != "/run/agenix/${secret.name}" ] && mkdir -p "$(dirname "${secret.path}")"
${if secret ? file then ''
(
umask u=r,g=,o=
LANG=${config.i18n.defaultLocale} ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secret.file}"
)
'' else ''
${pkgs.perl}/bin/perl -pe '${
lib.concatStringsSep "; "
(map (s:
''$match=q[@${s.name}@];'' +
''$replace=substr(qx[cat ${s.path}], 0, -1);'' +
''s/$match/$replace/ge'') secret.secrets)
}' ${secret.template} > "$TMP_FILE"
''}
chmod ${secret.mode} "$TMP_FILE"
chown ${secret.owner}:${secret.group} "$TMP_FILE"
mv -f "$TMP_FILE" "$_truePath"

${optionalString secretType.symlink ''
[ "${secretType.path}" != "/run/agenix/${secretType.name}" ] && ln -sfn "/run/agenix/${secretType.name}" "${secretType.path}"
${optionalString secret.symlink ''
[ "${secret.path}" != "/run/agenix/${secret.name}" ] && ln -sfn "/run/agenix/${secret.name}" "${secret.path}"
''}
'';

Expand All @@ -47,7 +63,16 @@ let
nonRootSecrets = builtins.filter isNotRootSecret (builtins.attrValues cfg.secrets);
installNonRootSecrets = builtins.concatStringsSep "\n" ([ "echo '[agenix] decrypting non-root secrets...'" ] ++ (map installSecret nonRootSecrets));

secretType = types.submodule ({ config, ... }: {
isRootDerivedSecret = st: isRootSecret st && builtins.all isRootSecret st.secrets;
isNotRootDerivedSecret = st: !(isRootDerivedSecret st);

rootDerivedSecrets = builtins.filter isRootDerivedSecret (builtins.attrValues cfg.derivedSecrets);
installRootDerivedSecrets = builtins.concatStringsSep "\n" ([ "echo '[agenix] constructing root derived secrets...'" ] ++ (map installSecret rootDerivedSecrets));

nonRootDerivedSecrets = builtins.filter isNotRootDerivedSecret (builtins.attrValues cfg.derivedSecrets);
installNonRootDerivedSecrets = builtins.concatStringsSep "\n" ([ "echo '[agenix] constructing non-root derived secrets...'" ] ++ (map installSecret nonRootDerivedSecrets));

baseSecretType = types.submodule ({ config, ... }: {
options = {
name = mkOption {
type = types.str;
Expand All @@ -56,12 +81,6 @@ let
Name of the file used in /run/agenix
'';
};
file = mkOption {
type = types.path;
description = ''
Age file the secret is loaded from.
'';
};
path = mkOption {
type = types.str;
default = "/run/agenix/${config.name}";
Expand Down Expand Up @@ -90,9 +109,53 @@ let
Group of the decrypted secret.
'';
};
action = mkOption {
type = types.str;
default = "";
description = "A script to run when secret is updated.";
};
services = mkOption {
type = types.listOf types.str;
default = [];
description = "The systemd services that uses this secret. Will be restarted when the secret changes.";
example = "[ wireguard-wg0 ]";
};
symlink = mkEnableOption "symlinking secrets to their destination" // { default = true; };
};
});

secretType = types.submodule {
imports = baseSecretType.getSubModules;
options = {
file = mkOption {
type = types.path;
description = ''
Age file the secret is loaded from.
'';
};
};
};

derivedSecretType = types.submodule {
imports = baseSecretType.getSubModules;
options = {
template = mkOption {
type = types.path;
description = ''
A template to insert other secrets into.
'';
example = ''builtins.toFile "secret-template" "@secret1@ @secret2@"'';
};
secrets = mkOption {
type = types.listOf secretType;
default = [];
description = ''
A list of secrets available to the template.
'';
example = ''with config.age.secrets; [ secret1 secret2 ]'';
};
};
};
in
{

Expand All @@ -115,6 +178,13 @@ in
Attrset of secrets.
'';
};
derivedSecrets = mkOption {
type = types.attrsOf derivedSecretType;
default = { };
description = ''
Attrset of secrets derived from a template applied to secrets from `age.secrets`.
'';
};
secretsMountPoint = mkOption {
type = types.addCheck types.str
(s:
Expand Down Expand Up @@ -172,7 +242,7 @@ in
# Secrets with root owner and group can be installed before users
# exist. This allows user password files to be encrypted.
system.activationScripts.agenixRoot = {
text = installRootOwnedSecrets;
text = installRootOwnedSecrets + "\n" + installRootDerivedSecrets;
deps = [ "agenixMountSecrets" "specialfs" ];
};
system.activationScripts.users.deps = [ "agenixRoot" ];
Expand All @@ -192,7 +262,7 @@ in

# Other secrets need to wait for users and groups to exist.
system.activationScripts.agenix = {
text = installNonRootSecrets;
text = installNonRootSecrets + "\n" + installNonRootDerivedSecrets;
deps = [
"users"
"groups"
Expand All @@ -201,6 +271,45 @@ in
"agenixChownKeys"
];
};
};

systemd.services =
let
hashFile = builtins.hashFile "sha256";
secretServices = { name, action, services, restartTriggers }:
lib.mkMerge [
(genAttrs services (_: { inherit restartTriggers; }))
(mkIf (action != "") {
"agenix-${name}-action" = {
inherit restartTriggers;

# We execute the action on reload so that it doesn't happen at
# startup. The only disadvantage is that it won't trigger the
# first time the service is created.
reload = action;
reloadIfChanged = true;

serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};

script = " "; # systemd complains if we only set ExecReload

# Give it a reason for starting
wantedBy = [ "multi-user.target" ];
};
})
];
in
lib.mkMerge (map secretServices
((mapAttrsToList (name: { file, action, services, path, mode, owner, group, ... }: {
inherit action services name;
restartTriggers = [ (hashFile file) path mode owner group ];
}) cfg.secrets)
++
(mapAttrsToList (name: { secrets, template, action, services, path, mode, owner, group, ... }: {
inherit action services name;
restartTriggers = map ({ file, ... }: hashFile file) secrets ++ [ template path mode owner group ];
}) cfg.derivedSecrets)));
};
}