Merge pull request #180 from ambroisie/add-home-manager

Add home-manager module
This commit is contained in:
Ryan Mulligan 2023-05-11 21:38:43 -07:00 committed by GitHub
commit 92197270a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 382 additions and 25 deletions

View file

@ -25,8 +25,17 @@ jobs:
- run: nix build .#doc - run: nix build .#doc
- run: nix fmt . -- --check - run: nix fmt . -- --check
- run: nix flake check - run: nix flake check
- run: | - name: "Install nix-darwin module"
run: |
system=$(nix build --no-link --print-out-paths .#checks.x86_64-darwin.integration) system=$(nix build --no-link --print-out-paths .#checks.x86_64-darwin.integration)
${system}/activate-user ${system}/activate-user
sudo ${system}/activate sudo ${system}/activate
- run: sudo /run/current-system/sw/bin/agenix-integration - name: "Test nix-darwin module"
run: |
sudo /run/current-system/sw/bin/agenix-integration
- name: "Test home-manager module"
run: |
# Do the job of `home-manager switch` in-line to avoid rate limiting
nix build .#homeConfigurations.integration-darwin.activationPackage
./result/activate
~/agenix-home-integration/bin/agenix-home-integration

View file

@ -21,6 +21,26 @@
"type": "github" "type": "github"
} }
}, },
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1682203081,
"narHash": "sha256-kRL4ejWDhi0zph/FpebFYhzqlOBrk0Pl3dzGEKSAlEw=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "32d3e39c491e2f91152c84f8ad8b003420eab0a1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1677676435, "lastModified": 1677676435,
@ -40,6 +60,7 @@
"root": { "root": {
"inputs": { "inputs": {
"darwin": "darwin", "darwin": "darwin",
"home-manager": "home-manager",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
} }

View file

@ -7,12 +7,17 @@
url = "github:lnl7/nix-darwin/master"; url = "github:lnl7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,
darwin, darwin,
home-manager,
}: let }: let
agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {}; agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
doc = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {}; doc = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {};
@ -23,6 +28,9 @@
darwinModules.age = import ./modules/age.nix; darwinModules.age = import ./modules/age.nix;
darwinModules.default = self.darwinModules.age; darwinModules.default = self.darwinModules.age;
homeManagerModules.age = import ./modules/age-home.nix;
homeManagerModules.default = self.homeManagerModules.age;
overlays.default = import ./overlay.nix; overlays.default = import ./overlay.nix;
formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra; formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra;
@ -49,24 +57,46 @@
packages.x86_64-linux.agenix = agenix "x86_64-linux"; packages.x86_64-linux.agenix = agenix "x86_64-linux";
packages.x86_64-linux.default = self.packages.x86_64-linux.agenix; packages.x86_64-linux.default = self.packages.x86_64-linux.agenix;
packages.x86_64-linux.doc = doc "x86_64-linux"; packages.x86_64-linux.doc = doc "x86_64-linux";
checks.x86_64-linux.integration = import ./test/integration.nix {
inherit nixpkgs; checks =
nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
integration =
(darwin.lib.darwinSystem {
inherit system;
modules = [
./test/integration_darwin.nix
"${darwin.outPath}/pkgs/darwin-installer/installer.nix"
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; pkgs = nixpkgs.legacyPackages.x86_64-linux;
system = "x86_64-linux"; system = "x86_64-linux";
}; };
checks."aarch64-darwin".integration = };
(darwin.lib.darwinSystem {
system = "aarch64-darwin";
modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.nix"];
})
.system;
checks."x86_64-darwin".integration =
(darwin.lib.darwinSystem {
system = "x86_64-darwin";
modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.nix"];
})
.system;
darwinConfigurations.integration.system = self.checks."x86_64-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];
};
});
}; };
} }

234
modules/age-home.nix Normal file
View file

@ -0,0 +1,234 @@
{
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 = [
"${config.home.homeDirectory}/.ssh/id_ed25519"
"${config.home.homeDirectory}/.ssh/id_rsa"
];
defaultText = litteralExpression ''
[
"''${config.home.homeDirectory}/.ssh/id_ed25519"
"''${config.home.homeDirectory}/.ssh/id_rsa"
]
'';
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";
};
};
};
}

View file

@ -1,10 +1,17 @@
# Do not copy this! It is insecure. This is only okay because we are testing. # Do not copy this! It is insecure. This is only okay because we are testing.
{ {
system.activationScripts.extraUserActivation.text = '' system.activationScripts.extraUserActivation.text = ''
echo "Installing SSH host key" echo "Installing system SSH host key"
sudo cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub sudo cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub
sudo cp ${../example_keys/system1} /etc/ssh/ssh_host_ed25519_key sudo cp ${../example_keys/system1} /etc/ssh/ssh_host_ed25519_key
sudo chmod 644 /etc/ssh/ssh_host_ed25519_key.pub sudo chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
sudo chmod 600 /etc/ssh/ssh_host_ed25519_key sudo chmod 600 /etc/ssh/ssh_host_ed25519_key
echo "Installing user SSH host key"
mkdir -p $HOME/.ssh
cp ${../example_keys/user1.pub} $HOME/.ssh/id_ed25519.pub
cp ${../example_keys/user1} $HOME/.ssh/id_ed25519
chmod 644 $HOME/.ssh/id_ed25519.pub
chmod 600 $HOME/.ssh/id_ed25519
''; '';
} }

View file

@ -6,6 +6,7 @@
config = {}; config = {};
}, },
system ? builtins.currentSystem, system ? builtins.currentSystem,
home-manager ? <home-manager>,
}: }:
pkgs.nixosTest { pkgs.nixosTest {
name = "agenix-integration"; name = "agenix-integration";
@ -18,6 +19,7 @@ pkgs.nixosTest {
imports = [ imports = [
../modules/age.nix ../modules/age.nix
./install_ssh_host_keys.nix ./install_ssh_host_keys.nix
"${home-manager}/nixos"
]; ];
services.openssh.enable = true; services.openssh.enable = true;
@ -43,11 +45,28 @@ pkgs.nixosTest {
}; };
}; };
}; };
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;
};
};
};
}; };
testScript = let testScript = let
user = "user1"; user = "user1";
password = "password1234"; password = "password1234";
secret2 = "world!";
in '' in ''
system1.wait_for_unit("multi-user.target") system1.wait_for_unit("multi-user.target")
system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'") system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
@ -65,6 +84,9 @@ pkgs.nixosTest {
system1.send_chars("whoami > /tmp/1\n") system1.send_chars("whoami > /tmp/1\n")
system1.wait_for_file("/tmp/1") system1.wait_for_file("/tmp/1")
assert "${user}" in system1.succeed("cat /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")
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}'"

View file

@ -8,7 +8,7 @@
testScript = pkgs.writeShellApplication { testScript = pkgs.writeShellApplication {
name = "agenix-integration"; name = "agenix-integration";
text = '' text = ''
grep ${secret} ${config.age.secrets.secret1.path} grep "${secret}" "${config.age.secrets.system-secret.path}"
''; '';
}; };
in { in {
@ -19,9 +19,10 @@ in {
services.nix-daemon.enable = true; services.nix-daemon.enable = true;
age.identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"]; age = {
identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
age.secrets.secret1.file = ../example/secret1.age; secrets.system-secret.file = ../example/secret1.age;
};
environment.systemPackages = [testScript]; environment.systemPackages = [testScript];
} }

View file

@ -0,0 +1,33 @@
{
pkgs,
config,
options,
lib,
...
}: {
imports = [../modules/age-home.nix];
age = {
identityPaths = options.age.identityPaths.default ++ ["/Users/user1/.ssh/this_key_wont_exist"];
secrets.user-secret.file = ../example/secret2.age;
};
home = rec {
username = "runner";
homeDirectory = lib.mkForce "/Users/${username}";
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')
'';
};
};
}