format nix with rfc style

This commit is contained in:
Ryan Mulligan 2025-08-04 09:26:41 -07:00
parent 856df6f692
commit 0814fdc0de
13 changed files with 500 additions and 453 deletions

View file

@ -14,7 +14,7 @@ jobs:
extra-experimental-features = recursive-nix nix-command flakes
- run: nix build
- run: nix build .#doc
- run: nix fmt . -- --check
- run: nix fmt . -- --ci
- run: nix flake check
tests-darwin:
runs-on: macos-latest
@ -27,7 +27,7 @@ jobs:
extra-experimental-features = recursive-nix nix-command flakes
- run: nix build
- run: nix build .#doc
- run: nix fmt . -- --check
- run: nix fmt . -- --ci
- run: nix flake check
- name: "Install nix-darwin module"
run: |

View file

@ -1,3 +1,6 @@
{pkgs ? import <nixpkgs> {}}: {
agenix = pkgs.callPackage ./pkgs/agenix.nix {};
{
pkgs ? import <nixpkgs> { },
}:
{
agenix = pkgs.callPackage ./pkgs/agenix.nix { };
}

View file

@ -1,13 +1,23 @@
let
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
system1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE";
in {
"secret1.age".publicKeys = [user1 system1];
"secret2.age".publicKeys = [user1];
"passwordfile-user1.age".publicKeys = [user1 system1];
"-leading-hyphen-filename.age".publicKeys = [user1 system1];
in
{
"secret1.age".publicKeys = [
user1
system1
];
"secret2.age".publicKeys = [ user1 ];
"passwordfile-user1.age".publicKeys = [
user1
system1
];
"-leading-hyphen-filename.age".publicKeys = [
user1
system1
];
"armored-secret.age" = {
publicKeys = [user1];
publicKeys = [ user1 ];
armor = true;
};
}

124
flake.nix
View file

@ -14,76 +14,78 @@
systems.url = "github:nix-systems/default";
};
outputs = {
self,
nixpkgs,
darwin,
home-manager,
systems,
}: let
eachSystem = nixpkgs.lib.genAttrs (import systems);
in {
nixosModules.age = ./modules/age.nix;
nixosModules.default = self.nixosModules.age;
outputs =
{
self,
nixpkgs,
darwin,
home-manager,
systems,
}:
let
eachSystem = nixpkgs.lib.genAttrs (import systems);
in
{
nixosModules.age = ./modules/age.nix;
nixosModules.default = self.nixosModules.age;
darwinModules.age = ./modules/age.nix;
darwinModules.default = self.darwinModules.age;
darwinModules.age = ./modules/age.nix;
darwinModules.default = self.darwinModules.age;
homeManagerModules.age = ./modules/age-home.nix;
homeManagerModules.default = self.homeManagerModules.age;
homeManagerModules.age = ./modules/age-home.nix;
homeManagerModules.default = self.homeManagerModules.age;
overlays.default = import ./overlay.nix;
overlays.default = import ./overlay.nix;
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.nixfmt-tree);
packages = eachSystem (system: {
agenix = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
doc = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {inherit self;};
default = self.packages.${system}.agenix;
});
packages = eachSystem (system: {
agenix = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix { };
doc = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix { inherit self; };
default = self.packages.${system}.agenix;
});
checks =
nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
integration =
(darwin.lib.darwinSystem {
inherit system;
modules = [
./test/integration_darwin.nix
checks =
nixpkgs.lib.genAttrs [ "aarch64-darwin" "x86_64-darwin" ] (system: {
integration =
(darwin.lib.darwinSystem {
inherit system;
modules = [
./test/integration_darwin.nix
# Allow new-style nix commands in CI
{nix.extraOptions = "experimental-features = nix-command flakes";}
# Allow new-style nix commands in CI
{ nix.extraOptions = "experimental-features = nix-command flakes"; }
home-manager.darwinModules.home-manager
{
home-manager = {
verbose = true;
useGlobalPkgs = true;
useUserPackages = true;
backupFileExtension = "hmbak";
users.runner = ./test/integration_hm_darwin.nix;
};
}
];
})
.system;
})
// {
x86_64-linux.integration = import ./test/integration.nix {
inherit nixpkgs home-manager;
pkgs = nixpkgs.legacyPackages.x86_64-linux;
system = "x86_64-linux";
home-manager.darwinModules.home-manager
{
home-manager = {
verbose = true;
useGlobalPkgs = true;
useUserPackages = true;
backupFileExtension = "hmbak";
users.runner = ./test/integration_hm_darwin.nix;
};
}
];
}).system;
})
// {
x86_64-linux.integration = import ./test/integration.nix {
inherit nixpkgs home-manager;
pkgs = nixpkgs.legacyPackages.x86_64-linux;
system = "x86_64-linux";
};
};
};
darwinConfigurations.integration-x86_64.system = self.checks.x86_64-darwin.integration;
darwinConfigurations.integration-aarch64.system = self.checks.aarch64-darwin.integration;
darwinConfigurations.integration-x86_64.system = self.checks.x86_64-darwin.integration;
darwinConfigurations.integration-aarch64.system = self.checks.aarch64-darwin.integration;
# Work-around for https://github.com/nix-community/home-manager/issues/3075
legacyPackages = nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
homeConfigurations.integration-darwin = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.${system};
modules = [./test/integration_hm_darwin.nix];
};
});
};
# Work-around for https://github.com/nix-community/home-manager/issues/3075
legacyPackages = nixpkgs.lib.genAttrs [ "aarch64-darwin" "x86_64-darwin" ] (system: {
homeConfigurations.integration-darwin = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.${system};
modules = [ ./test/integration_hm_darwin.nix ];
};
});
};
}

View file

@ -5,7 +5,8 @@
pkgs,
...
}:
with lib; let
with lib;
let
cfg = config.age;
ageBin = lib.getExe config.age.package;
@ -22,13 +23,14 @@ with lib; let
setTruePath = secretType: ''
${
if secretType.symlink
then ''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
''
else ''
_truePath="${secretType.path}"
''
if secretType.symlink then
''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
''
else
''
_truePath="${secretType.path}"
''
}
'';
@ -54,7 +56,9 @@ with lib; let
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}"
LANG=${
config.i18n.defaultLocale or "C"
} ${ageBin} --decrypt "''${IDENTITIES[@]}" -o "$TMP_FILE" "${secretType.file}"
)
chmod ${secretType.mode} "$TMP_FILE"
mv -f "$TMP_FILE" "$_truePath"
@ -65,12 +69,9 @@ with lib; let
''}
'';
testIdentities =
map
(path: ''
test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
'')
cfg.identityPaths;
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)"
@ -85,81 +86,89 @@ with lib; let
'';
installSecrets = builtins.concatStringsSep "\n" (
["echo '[agenix] decrypting secrets...'"]
[ "echo '[agenix] decrypting secrets...'" ]
++ testIdentities
++ (map installSecret (builtins.attrValues cfg.secrets))
++ [cleanupAndLink]
++ [ cleanupAndLink ]
);
secretType = types.submodule ({
config,
name,
...
}: {
options = {
name = mkOption {
type = types.str;
default = name;
description = ''
Name of the file used in ''${cfg.secretsDir}
'';
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;
};
};
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
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}";
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:
userDirectoryDescription =
dir:
literalExpression ''
"''${XDG_RUNTIME_DIR}"/''${dir} on linux or "$(getconf DARWIN_USER_TEMP_DIR)"/''${dir} on darwin.
'';
in {
in
{
options.age = {
package = mkPackageOption pkgs "age" {};
package = mkPackageOption pkgs "age" { };
secrets = mkOption {
type = types.attrsOf secretType;
default = {};
default = { };
description = ''
Attrset of secrets.
'';
@ -200,10 +209,10 @@ in {
};
};
config = mkIf (cfg.secrets != {}) {
config = mkIf (cfg.secrets != { }) {
assertions = [
{
assertion = cfg.identityPaths != [];
assertion = cfg.identityPaths != [ ];
message = "age.identityPaths must be set.";
}
];
@ -216,13 +225,13 @@ in {
Type = "oneshot";
ExecStart = mountingScript;
};
Install.WantedBy = ["default.target"];
Install.WantedBy = [ "default.target" ];
};
launchd.agents.activate-agenix = {
enable = true;
config = {
ProgramArguments = [mountingScript];
ProgramArguments = [ mountingScript ];
KeepAlive = {
Crashed = false;
SuccessfulExit = false;

View file

@ -5,34 +5,37 @@
pkgs,
...
}:
with lib; let
with lib;
let
cfg = config.age;
isDarwin = lib.attrsets.hasAttrByPath ["environment" "darwinConfig"] options;
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);
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
'';
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 ))
@ -44,10 +47,7 @@ with lib; let
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
'';
chownGroup =
if isDarwin
then "admin"
else "keys";
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 = ''
@ -56,13 +56,14 @@ with lib; let
setTruePath = secretType: ''
${
if secretType.symlink
then ''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
''
else ''
_truePath="${secretType.path}"
''
if secretType.symlink then
''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
''
else
''
_truePath="${secretType.path}"
''
}
'';
@ -87,7 +88,9 @@ with lib; let
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}"
LANG=${
config.i18n.defaultLocale or "C"
} ${ageBin} --decrypt "''${IDENTITIES[@]}" -o "$TMP_FILE" "${secretType.file}"
)
chmod ${secretType.mode} "$TMP_FILE"
mv -f "$TMP_FILE" "$_truePath"
@ -97,12 +100,9 @@ with lib; let
''}
'';
testIdentities =
map
(path: ''
test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
'')
cfg.identityPaths;
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)"
@ -117,10 +117,10 @@ with lib; let
'';
installSecrets = builtins.concatStringsSep "\n" (
["echo '[agenix] decrypting secrets...'"]
[ "echo '[agenix] decrypting secrets...'" ]
++ testIdentities
++ (map installSecret (builtins.attrValues cfg.secrets))
++ [cleanupAndLink]
++ [ cleanupAndLink ]
);
chownSecret = secretType: ''
@ -129,67 +129,73 @@ with lib; let
'';
chownSecrets = builtins.concatStringsSep "\n" (
["echo '[agenix] chowning...'"]
++ [chownMountPoint]
[ "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`
'';
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;
};
};
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 {
}
);
in
{
imports = [
(mkRenamedOptionModule ["age" "sshKeyPaths"] ["age" "identityPaths"])
(mkRenamedOptionModule [ "age" "sshKeyPaths" ] [ "age" "identityPaths" ])
];
options.age = {
@ -205,7 +211,7 @@ in {
};
secrets = mkOption {
type = types.attrsOf secretType;
default = {};
default = { };
description = ''
Attrset of secrets.
'';
@ -219,12 +225,14 @@ in {
};
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)";};
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`
@ -233,14 +241,17 @@ in {
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 [];
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 [
@ -257,11 +268,11 @@ in {
};
};
config = mkIf (cfg.secrets != {}) (mkMerge [
config = mkIf (cfg.secrets != { }) (mkMerge [
{
assertions = [
{
assertion = cfg.identityPaths != [];
assertion = cfg.identityPaths != [ ];
message = "age.identityPaths must be set, for example by enabling openssh.";
}
];
@ -270,20 +281,18 @@ in {
# 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"];
wantedBy = [ "sysinit.target" ];
after = [ "systemd-sysusers.service" ];
unitConfig.DefaultDependencies = "no";
path = [pkgs.mount];
path = [ pkgs.mount ];
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "agenix-install" (
concatLines [
newGeneration
installSecrets
chownSecrets
]
);
ExecStart = pkgs.writeShellScript "agenix-install" (concatLines [
newGeneration
installSecrets
chownSecrets
]);
RemainAfterExit = true;
};
};
@ -308,7 +317,7 @@ in {
};
# So user passwords can be encrypted.
users.deps = ["agenixInstall"];
users.deps = [ "agenixInstall" ];
# Change ownership and group after users and groups are made.
agenixChown = {
@ -322,7 +331,7 @@ in {
# So other activation scripts can depend on agenix being done.
agenix = {
text = "";
deps = ["agenixChown"];
deps = [ "agenixChown" ];
};
};
})

View file

@ -1,3 +1,3 @@
final: prev: {
agenix = prev.callPackage ./pkgs/agenix.nix {};
agenix = prev.callPackage ./pkgs/agenix.nix { };
}

View file

@ -9,57 +9,58 @@
replaceVars,
ageBin ? "${age}/bin/age",
shellcheck,
}: let
}:
let
bin = "${placeholder "out"}/bin/agenix";
in
stdenv.mkDerivation rec {
pname = "agenix";
version = "0.15.0";
src = replaceVars ./agenix.sh {
inherit ageBin version;
jqBin = "${jq}/bin/jq";
nixInstantiate = "${nix}/bin/nix-instantiate";
mktempBin = "${mktemp}/bin/mktemp";
diffBin = "${diffutils}/bin/diff";
};
dontUnpack = true;
doInstallCheck = true;
installCheckInputs = [shellcheck];
postInstallCheck = ''
shellcheck ${bin}
${bin} -h | grep ${version}
stdenv.mkDerivation rec {
pname = "agenix";
version = "0.15.0";
src = replaceVars ./agenix.sh {
inherit ageBin version;
jqBin = "${jq}/bin/jq";
nixInstantiate = "${nix}/bin/nix-instantiate";
mktempBin = "${mktemp}/bin/mktemp";
diffBin = "${diffutils}/bin/diff";
};
dontUnpack = true;
doInstallCheck = true;
installCheckInputs = [ shellcheck ];
postInstallCheck = ''
shellcheck ${bin}
${bin} -h | grep ${version}
test_tmp=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
export HOME="$test_tmp/home"
export NIX_STORE_DIR="$test_tmp/nix/store"
export NIX_STATE_DIR="$test_tmp/nix/var"
mkdir -p "$HOME" "$NIX_STORE_DIR" "$NIX_STATE_DIR"
function cleanup {
rm -rf "$test_tmp"
}
trap "cleanup" 0 2 3 15
test_tmp=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
export HOME="$test_tmp/home"
export NIX_STORE_DIR="$test_tmp/nix/store"
export NIX_STATE_DIR="$test_tmp/nix/var"
mkdir -p "$HOME" "$NIX_STORE_DIR" "$NIX_STATE_DIR"
function cleanup {
rm -rf "$test_tmp"
}
trap "cleanup" 0 2 3 15
mkdir -p $HOME/.ssh
cp -r "${../example}" $HOME/secrets
chmod -R u+rw $HOME/secrets
(
umask u=rw,g=r,o=r
cp ${../example_keys/user1.pub} $HOME/.ssh/id_ed25519.pub
chown $UID $HOME/.ssh/id_ed25519.pub
)
(
umask u=rw,g=,o=
cp ${../example_keys/user1} $HOME/.ssh/id_ed25519
chown $UID $HOME/.ssh/id_ed25519
)
mkdir -p $HOME/.ssh
cp -r "${../example}" $HOME/secrets
chmod -R u+rw $HOME/secrets
(
umask u=rw,g=r,o=r
cp ${../example_keys/user1.pub} $HOME/.ssh/id_ed25519.pub
chown $UID $HOME/.ssh/id_ed25519.pub
)
(
umask u=rw,g=,o=
cp ${../example_keys/user1} $HOME/.ssh/id_ed25519
chown $UID $HOME/.ssh/id_ed25519
)
cd $HOME/secrets
test $(${bin} -d secret1.age) = "hello"
'';
cd $HOME/secrets
test $(${bin} -d secret1.age) = "hello"
'';
installPhase = ''
install -D $src ${bin}
'';
installPhase = ''
install -D $src ${bin}
'';
meta.description = "age-encrypted secrets for NixOS";
}
meta.description = "age-encrypted secrets for NixOS";
}

View file

@ -6,6 +6,6 @@
stdenvNoCC.mkDerivation rec {
name = "agenix-doc";
src = ../doc;
phases = ["mmdocPhase"];
phases = [ "mmdocPhase" ];
mmdocPhase = "${mmdoc}/bin/mmdoc agenix $src $out";
}

View file

@ -1,6 +1,7 @@
# Do not copy this! It is insecure. This is only okay because we are testing.
{config, ...}: {
system.activationScripts.agenixInstall.deps = ["installSSHHostKeys"];
{ config, ... }:
{
system.activationScripts.agenixInstall.deps = [ "installSSHHostKeys" ];
system.activationScripts.installSSHHostKeys.text = ''
USER1_UID="${toString config.users.users.user1.uid}"

View file

@ -1,137 +1,142 @@
{
nixpkgs ? <nixpkgs>,
pkgs ?
import <nixpkgs> {
inherit system;
config = {};
},
pkgs ? import <nixpkgs> {
inherit system;
config = { };
},
system ? builtins.currentSystem,
home-manager ? <home-manager>,
}:
pkgs.nixosTest {
name = "agenix-integration";
nodes.system1 = {
config,
pkgs,
options,
...
}: {
imports = [
../modules/age.nix
./install_ssh_host_keys.nix
"${home-manager}/nixos"
];
services.openssh.enable = true;
age.secrets = {
passwordfile-user1.file = ../example/passwordfile-user1.age;
leading-hyphen.file = ../example/-leading-hyphen-filename.age;
};
age.identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
environment.systemPackages = [
(pkgs.callPackage ../pkgs/agenix.nix {})
];
users = {
mutableUsers = false;
users = {
user1 = {
isNormalUser = true;
passwordFile = config.age.secrets.passwordfile-user1.path;
uid = 1000;
};
};
};
home-manager.users.user1 = {options, ...}: {
nodes.system1 =
{
config,
pkgs,
options,
...
}:
{
imports = [
../modules/age-home.nix
../modules/age.nix
./install_ssh_host_keys.nix
"${home-manager}/nixos"
];
home.stateVersion = pkgs.lib.trivial.release;
services.openssh.enable = true;
age = {
identityPaths = options.age.identityPaths.default ++ ["/home/user1/.ssh/this_key_wont_exist"];
secrets.secret2 = {
# Only decryptable by user1's key
file = ../example/secret2.age;
};
secrets.secret2Path = {
file = ../example/secret2.age;
path = "/home/user1/secret2";
};
secrets.armored-secret = {
file = ../example/armored-secret.age;
age.secrets = {
passwordfile-user1.file = ../example/passwordfile-user1.age;
leading-hyphen.file = ../example/-leading-hyphen-filename.age;
};
age.identityPaths = options.age.identityPaths.default ++ [ "/etc/ssh/this_key_wont_exist" ];
environment.systemPackages = [
(pkgs.callPackage ../pkgs/agenix.nix { })
];
users = {
mutableUsers = false;
users = {
user1 = {
isNormalUser = true;
passwordFile = config.age.secrets.passwordfile-user1.path;
uid = 1000;
};
};
};
home-manager.users.user1 =
{ options, ... }:
{
imports = [
../modules/age-home.nix
];
home.stateVersion = pkgs.lib.trivial.release;
age = {
identityPaths = options.age.identityPaths.default ++ [ "/home/user1/.ssh/this_key_wont_exist" ];
secrets.secret2 = {
# Only decryptable by user1's key
file = ../example/secret2.age;
};
secrets.secret2Path = {
file = ../example/secret2.age;
path = "/home/user1/secret2";
};
secrets.armored-secret = {
file = ../example/armored-secret.age;
};
};
};
};
};
testScript = let
user = "user1";
password = "password1234";
secret2 = "world!";
hyphen-secret = "filename started with hyphen";
armored-secret = "Hello World!";
in ''
system1.wait_for_unit("multi-user.target")
system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
system1.sleep(2)
system1.send_key("alt-f2")
system1.wait_until_succeeds("[ $(fgconsole) = 2 ]")
system1.wait_for_unit("getty@tty2.service")
system1.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
system1.wait_until_tty_matches("2", "login: ")
system1.send_chars("${user}\n")
system1.wait_until_tty_matches("2", "login: ${user}")
system1.wait_until_succeeds("pgrep login")
system1.sleep(2)
system1.send_chars("${password}\n")
system1.send_chars("whoami > /tmp/1\n")
system1.wait_for_file("/tmp/1")
assert "${user}" in system1.succeed("cat /tmp/1")
system1.send_chars("cat /run/user/$(id -u)/agenix/secret2 > /tmp/2\n")
system1.wait_for_file("/tmp/2")
assert "${secret2}" in system1.succeed("cat /tmp/2")
system1.send_chars("cat /run/user/$(id -u)/agenix/armored-secret > /tmp/3\n")
system1.wait_for_file("/tmp/3")
assert "${armored-secret}" in system1.succeed("cat /tmp/3")
testScript =
let
user = "user1";
password = "password1234";
secret2 = "world!";
hyphen-secret = "filename started with hyphen";
armored-secret = "Hello World!";
in
''
system1.wait_for_unit("multi-user.target")
system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
system1.sleep(2)
system1.send_key("alt-f2")
system1.wait_until_succeeds("[ $(fgconsole) = 2 ]")
system1.wait_for_unit("getty@tty2.service")
system1.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
system1.wait_until_tty_matches("2", "login: ")
system1.send_chars("${user}\n")
system1.wait_until_tty_matches("2", "login: ${user}")
system1.wait_until_succeeds("pgrep login")
system1.sleep(2)
system1.send_chars("${password}\n")
system1.send_chars("whoami > /tmp/1\n")
system1.wait_for_file("/tmp/1")
assert "${user}" in system1.succeed("cat /tmp/1")
system1.send_chars("cat /run/user/$(id -u)/agenix/secret2 > /tmp/2\n")
system1.wait_for_file("/tmp/2")
assert "${secret2}" in system1.succeed("cat /tmp/2")
system1.send_chars("cat /run/user/$(id -u)/agenix/armored-secret > /tmp/3\n")
system1.wait_for_file("/tmp/3")
assert "${armored-secret}" in system1.succeed("cat /tmp/3")
assert "${hyphen-secret}" in system1.succeed("cat /run/agenix/leading-hyphen")
assert "${hyphen-secret}" in system1.succeed("cat /run/agenix/leading-hyphen")
userDo = lambda input : f"sudo -u user1 -- bash -c 'set -eou pipefail; cd /tmp/secrets; {input}'"
userDo = lambda input : f"sudo -u user1 -- bash -c 'set -eou pipefail; cd /tmp/secrets; {input}'"
before_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
print(system1.succeed(userDo('agenix -r -i /home/user1/.ssh/id_ed25519')))
after_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
before_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
print(system1.succeed(userDo('agenix -r -i /home/user1/.ssh/id_ed25519')))
after_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
# Ensure we actually have hashes
for h in [before_hash, after_hash]:
assert len(h) == 2, "hash should be [hash, filename]"
assert h[1] == "passwordfile-user1.age", "filename is incorrect"
assert len(h[0].strip()) == 64, "hash length is incorrect"
assert before_hash[0] != after_hash[0], "hash did not change with rekeying"
# Ensure we actually have hashes
for h in [before_hash, after_hash]:
assert len(h) == 2, "hash should be [hash, filename]"
assert h[1] == "passwordfile-user1.age", "filename is incorrect"
assert len(h[0].strip()) == 64, "hash length is incorrect"
assert before_hash[0] != after_hash[0], "hash did not change with rekeying"
# user1 can edit passwordfile-user1.age
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
# user1 can edit passwordfile-user1.age
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
# user1 can edit even if bogus id_rsa present
system1.succeed(userDo("echo bogus > ~/.ssh/id_rsa"))
system1.fail(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age -i /home/user1/.ssh/id_ed25519"))
system1.succeed(userDo("rm ~/.ssh/id_rsa"))
# user1 can edit even if bogus id_rsa present
system1.succeed(userDo("echo bogus > ~/.ssh/id_rsa"))
system1.fail(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age -i /home/user1/.ssh/id_ed25519"))
system1.succeed(userDo("rm ~/.ssh/id_rsa"))
# user1 can edit a secret by piping in contents
system1.succeed(userDo("echo 'secret1234' | agenix -e passwordfile-user1.age"))
# user1 can edit a secret by piping in contents
system1.succeed(userDo("echo 'secret1234' | agenix -e passwordfile-user1.age"))
# and get it back out via --decrypt
assert "secret1234" in system1.succeed(userDo("agenix -d passwordfile-user1.age"))
# and get it back out via --decrypt
assert "secret1234" in system1.succeed(userDo("agenix -d passwordfile-user1.age"))
# finally, the plain text should not linger around anywhere in the filesystem.
system1.fail("grep -r secret1234 /tmp")
'';
# finally, the plain text should not linger around anywhere in the filesystem.
system1.fail("grep -r secret1234 /tmp")
'';
}

View file

@ -3,7 +3,8 @@
pkgs,
options,
...
}: let
}:
let
secret = "hello";
testScript = pkgs.writeShellApplication {
name = "agenix-integration";
@ -11,18 +12,19 @@
grep "${secret}" "${config.age.secrets.system-secret.path}"
'';
};
in {
in
{
imports = [
./install_ssh_host_keys_darwin.nix
../modules/age.nix
];
age = {
identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
identityPaths = options.age.identityPaths.default ++ [ "/etc/ssh/this_key_wont_exist" ];
secrets.system-secret.file = ../example/secret1.age;
};
environment.systemPackages = [testScript];
environment.systemPackages = [ testScript ];
system.stateVersion = 6;
}

View file

@ -4,11 +4,12 @@
options,
lib,
...
}: {
imports = [../modules/age-home.nix];
}:
{
imports = [ ../modules/age-home.nix ];
age = {
identityPaths = options.age.identityPaths.default ++ ["/Users/user1/.ssh/this_key_wont_exist"];
identityPaths = options.age.identityPaths.default ++ [ "/Users/user1/.ssh/this_key_wont_exist" ];
secrets.user-secret.file = ../example/secret2.age;
};
@ -18,16 +19,20 @@
stateVersion = lib.trivial.release;
};
home.file = let
name = "agenix-home-integration";
in {
${name}.source = pkgs.writeShellApplication {
inherit name;
text = let
secret = "world!";
in ''
diff -q "${config.age.secrets.user-secret.path}" <(printf '${secret}\n')
'';
home.file =
let
name = "agenix-home-integration";
in
{
${name}.source = pkgs.writeShellApplication {
inherit name;
text =
let
secret = "world!";
in
''
diff -q "${config.age.secrets.user-secret.path}" <(printf '${secret}\n')
'';
};
};
};
}