-
-
Save gallexme/9ade2bc91df4d4263ee006264e2f3b9d to your computer and use it in GitHub Desktop.
agenix devenv
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A flake-parts module for building and running Emanote sites | |
{ | |
self, | |
pkgs, | |
config, | |
lib, | |
flake-parts-lib, | |
options, | |
... | |
}: | |
with lib; let | |
cfg = config.age; | |
inherit (config.age) ageBin; | |
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}" | |
mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation" | |
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation" | |
''; | |
chownGroup = "$GID"; | |
# 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 | |
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 -sfn "${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 -sfn "${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 ${builtins.getEnv "UID"}:$GID "$_truePath" | |
''; | |
chownSecrets = builtins.concatStringsSep "\n" ( | |
["echo '[agenix] chowning...'"] | |
++ [chownMountPoint] | |
++ (map chownSecret (builtins.attrValues cfg.secrets)) | |
); | |
inherit | |
(flake-parts-lib) | |
mkPerSystemOption | |
; | |
inherit | |
(lib) | |
mkOption | |
types | |
; | |
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 = "0"; | |
defaultText = literalExpression '' | |
users.''${config.owner}.group or "0" | |
''; | |
description = '' | |
Group of the decrypted secret. | |
''; | |
}; | |
symlink = mkEnableOption "symlinking secrets to their destination" // {default = true;}; | |
}; | |
}); | |
concatMapAttrs = f: v: | |
foldl' mergeAttrs {} | |
( | |
attrValues | |
(mapAttrs f v) | |
); | |
in { | |
options.secrets = {}; | |
options.age = { | |
ageBin = mkOption { | |
type = types.str; | |
default = "${pkgs.rage}/bin/rage"; | |
defaultText = '' | |
"''${pkgs.rage}/bin/rage" | |
''; | |
description = '' | |
The age executable to use. | |
''; | |
}; | |
secrets = mkOption { | |
type = types.attrsOf secretType; | |
default = {}; | |
description = '' | |
Attrset of secrets. | |
''; | |
}; | |
secretsDir = mkOption { | |
type = types.str; | |
default = "./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 = "./agenix.d"; | |
description = '' | |
Where secrets are created before they are symlinked to {option}`age.secretsDir` | |
''; | |
}; | |
identityPaths = mkOption { | |
type = types.listOf types.str; | |
default = | |
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 (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 [ | |
{ | |
} | |
(optionalAttrs (!false) { | |
# Create a new directory full of secrets for symlinking (this helps | |
# ensure removed secrets are actually removed, or at least become | |
# invalid symlinks). | |
}) | |
(optionalAttrs (builtins.hasAttr "env" options) { | |
scripts.agenixNewGeneration.exec = newGeneration; | |
scripts.agenixInstall.exec = "${newGeneration}\n ${installSecrets}"; | |
# Change ownership and group after users and groups are made. | |
scripts.agenixChown.exec = chownSecrets; | |
# So other activation scripts can depend on agenix being done. | |
scripts.agenixSetup.exec = "${newGeneration}\n ${installSecrets} \n ${chownSecrets}"; #agenixChown; | |
env = concatMapAttrs (name: value: {${"TF_VAR_secret_" + name} = value.path;}) cfg.secrets; #toUpper | |
}) | |
(optionalAttrs false { | |
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; | |
}; | |
}; | |
}) | |
]); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
devenv.shells.default = { | |
name = "my-project"; | |
imports = [ | |
./agenix-custom.nix | |
# This is just like the imports in devenv.nix. | |
# See https://devenv.sh/guides/using-with-flake-parts/#import-a-devenv-module | |
# ./devenv-foo.nix | |
]; | |
age.secrets.oci_public.file = ./secrets/oci_public.pem.age; | |
age.secrets.oci.file = ./secrets/oci.pem.age; | |
age.secrets.cloudflare_token.file = ./secrets/cloudflare_token.age; | |
age.identityPaths = ["~/.ssh/id_rsa" "~/.ssh/nix_rsa" "~/.ssh/id_gitlab_shared"]; | |
}; | |
enterShell = '' | |
agenixSetup | |
``; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment