Skip to content

Commit

Permalink
Add home-manager module
Browse files Browse the repository at this point in the history
This is to update and fix the issues I saw in [1] and [2].

Using a service definition instead of an activation script should
resolve the issue about the secrets disappearing after rebooting.

Removed the `user` and `group` option as they do not make sense to me
for a home-manager module, which should target a single user. They can
always be added back if somebody comes screaming.

This is somewhat modeled after sops-nix's own module [3].

[1]: ryantm#58
[2]: ryantm#109
[3]: https://github.com/Mic92/sops-nix/blob/master/modules/home-manager/sops.nix
  • Loading branch information
ambroisie authored and n8henrie committed May 5, 2023
1 parent c6e7ff1 commit af5adeb
Show file tree
Hide file tree
Showing 2 changed files with 228 additions and 0 deletions.
3 changes: 3 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
darwinModules.age = import ./modules/age.nix;
darwinModules.default = self.darwinModules.age;

homeManagerModules.age = import ./modules/age-home.nix;
homeManagerModules.default = self.homeManagerModules.age;

overlays.default = import ./overlay.nix;

formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra;
Expand Down
225 changes: 225 additions & 0 deletions modules/age-home.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
{
config,
options,
lib,
pkgs,
...
}:
with lib; let
cfg = config.age;

ageBin = lib.getExe config.age.package;

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"
'';

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=()
# shellcheck disable=2043
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]
);

secretType = types.submodule ({
config,
name,
...
}: {
options = {
name = mkOption {
type = types.str;
default = name;
description = ''
Name of the file used in ''${cfg.secretsDir}
'';
};
file = mkOption {
type = types.path;
description = ''
Age file the secret is loaded from.
'';
};
path = mkOption {
type = types.str;
default = "${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.
'';
};
symlink = mkEnableOption "symlinking secrets to their destination" // {default = true;};
};
});

mountingScript = let
app = pkgs.writeShellApplication {
name = "agenix-home-manager-mount-secrets";
runtimeInputs = with pkgs; [coreutils];
text = ''
${newGeneration}
${installSecrets}
exit 0
'';
};
in
lib.getExe app;

userDirectory = dir: let
inherit (pkgs.stdenv.hostPlatform) isDarwin;
baseDir =
if isDarwin
then "$(getconf DARWIN_USER_TEMP_DIR)"
else "$XDG_RUNTIME_DIR";
in "${baseDir}/${dir}";

userDirectoryDescription = dir: ''
"$XDG_RUNTIME_DIR"/${dir} on linux or "$(getconf DARWIN_USER_TEMP_DIR)"/${dir} on darwin.
'';
in {
options.age = {
package = mkPackageOption pkgs "rage" {};

secrets = mkOption {
type = types.attrsOf secretType;
default = {};
description = ''
Attrset of secrets.
'';
};

identityPaths = mkOption {
type = types.listOf types.path;
default = [];
description = ''
Path to SSH keys to be used as identities in age decryption.
'';
};

secretsDir = mkOption {
type = types.str;
default = userDirectory "agenix";
defaultText = userDirectoryDescription "agenix";
description = ''
Folder where secrets are symlinked to
'';
};

secretsMountPoint = mkOption {
default = userDirectory "agenix.d";
defaultText = userDirectoryDescription "agenix.d";
description = ''
Where secrets are created before they are symlinked to ''${cfg.secretsDir}
'';
};
};

config = mkIf (cfg.secrets != {}) {
assertions = [
{
assertion = cfg.identityPaths != [];
message = "age.identityPaths must be set.";
}
];

systemd.user.services.agenix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
Unit = {
Description = "agenix activation";
};
Service = {
Type = "oneshot";
ExecStart = mountingScript;
};
Install.WantedBy = ["default.target"];
};

launchd.agents.activate-agenix = {
enable = true;
config = {
ProgramArguments = [mountingScript];
KeepAlive = {
Crashed = false;
SuccessfulExit = false;
};
RunAtLoad = true;
ProcessType = "Background";
StandardOutPath = "${config.home.homeDirectory}/Library/Logs/agenix/stdout";
StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/agenix/stderr";
};
};
};
}

0 comments on commit af5adeb

Please sign in to comment.