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

feature: try to add nix-darwin support #141

Merged
merged 12 commits into from
Jan 31, 2023
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ jobs:
- run: nix build
- run: nix fmt . -- --check
- run: nix flake check
- run: |
system=$(nix build --no-link --print-out-paths .#checks.x86_64-darwin.integration)
${system}/activate-user
sudo ${system}/activate
- run: sudo /run/current-system/sw/bin/agenix-integration
22 changes: 22 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 25 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
{
description = "Secret management with age";

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
darwin = {
url = "github:lnl7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
};

outputs = {
self,
nixpkgs,
darwin,
}: let
agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
in {
nixosModules.age = import ./modules/age.nix;
nixosModules.default = self.nixosModules.age;

darwinModules.age = import ./modules/age.nix;
darwinModules.default = self.darwinModules.age;

overlays.default = import ./overlay.nix;

formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra;
Expand All @@ -38,5 +48,19 @@
pkgs = nixpkgs.legacyPackages.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;
};
}
153 changes: 105 additions & 48 deletions modules/age.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
with lib; let
cfg = config.age;

isDarwin = builtins.hasAttr "darwinConfig" options.environment;

# we need at least rage 0.5.0 to support ssh keys
rage =
if lib.versionOlder pkgs.rage.version "0.5.0"
Expand All @@ -17,17 +19,40 @@ with lib; let

users = config.users.users;

mountCommand =
if isDarwin
then ''
if ! diskutil info "${cfg.secretsMountPoint}"; then
dev="$(hdiutil attach -nomount ram://1048576 | awk '{print $1}')"
newfs_hfs "$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 ))
echo "[agenix] creating new generation in ${cfg.secretsMountPoint}/$_agenix_generation"
mkdir -p "${cfg.secretsMountPoint}"
chmod 0751 "${cfg.secretsMountPoint}"
grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts || mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751
${mountCommand}
mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation"
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
'';

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 = ''
chown :${chownGroup} "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation"
'';

identities = builtins.concatStringsSep " " (map (path: "-i ${path}") cfg.identityPaths);

setTruePath = secretType: ''
Expand All @@ -52,7 +77,7 @@ 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} ${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"
Expand Down Expand Up @@ -92,12 +117,6 @@ with lib; let
chown ${secretType.owner}:${secretType.group} "$_truePath"
'';

# chown the secrets mountpoint and the current generation to the keys group
# instead of leaving it root:root.
chownMountPoint = ''
chown :keys "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation"
'';

chownSecrets = builtins.concatStringsSep "\n" (
["echo '[agenix] chowning...'"]
++ [chownMountPoint]
Expand Down Expand Up @@ -194,57 +213,95 @@ in {
identityPaths = mkOption {
type = types.listOf types.path;
default =
if config.services.openssh.enable
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 [];
description = ''
Path to SSH keys to be used as identities in age decryption.
'';
};
};

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

# Create a new directory full of secrets for symlinking (this helps
# ensure removed secrets are actually removed, or at least become
# invalid symlinks).
system.activationScripts.agenixNewGeneration = {
text = newGeneration;
deps = [
"specialfs"
config = mkIf (cfg.secrets != {}) (mkMerge [
{
assertions = [
{
assertion = cfg.identityPaths != [];
message = "age.identityPaths must be set.";
}
];
};
}

system.activationScripts.agenixInstall = {
text = installSecrets;
deps = [
"agenixNewGeneration"
"specialfs"
];
};
(optionalAttrs (!isDarwin) {
# Create a new directory full of secrets for symlinking (this helps
# ensure removed secrets are actually removed, or at least become
# invalid symlinks).
system.activationScripts.agenixNewGeneration = {
text = newGeneration;
deps = [
"specialfs"
];
};

# So user passwords can be encrypted.
system.activationScripts.users.deps = ["agenixInstall"];
system.activationScripts.agenixInstall = {
text = installSecrets;
deps = [
"agenixNewGeneration"
"specialfs"
];
};

# Change ownership and group after users and groups are made.
system.activationScripts.agenixChown = {
text = chownSecrets;
deps = [
"users"
"groups"
];
};
# So user passwords can be encrypted.
system.activationScripts.users.deps = ["agenixInstall"];

# So other activation scripts can depend on agenix being done.
system.activationScripts.agenix = {
text = "";
deps = ["agenixChown"];
};
};
# Change ownership and group after users and groups are made.
system.activationScripts.agenixChown = {
text = chownSecrets;
deps = [
"users"
"groups"
];
};

# So other activation scripts can depend on agenix being done.
system.activationScripts.agenix = {
text = "";
deps = ["agenixChown"];
};
})
(optionalAttrs isDarwin {
system.activationScripts = {
# Secrets with root owner and group can be installed before users
# exist. This allows user password files to be encrypted.
preActivation.text = builtins.concatStringsSep "\n" [
newGeneration
installSecrets
];

# Other secrets need to wait for users and groups to exist.
users.text = lib.mkAfter ''
${chownSecrets}
'';
};

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;
serviceConfig.KeepAlive.SuccessfulExit = false;
};
n8henrie marked this conversation as resolved.
Show resolved Hide resolved
})
]);
}
10 changes: 10 additions & 0 deletions test/install_ssh_host_keys_darwin.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Do not copy this! It is insecure. This is only okay because we are testing.
{
system.activationScripts.extraUserActivation.text = ''
echo "Installing SSH host key"
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 chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
sudo chmod 600 /etc/ssh/ssh_host_ed25519_key
'';
}
24 changes: 24 additions & 0 deletions test/integration_darwin.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
config,
pkgs,
...
}: let
secret = "hello";
testScript = pkgs.writeShellApplication {
name = "agenix-integration";
text = ''
grep ${secret} ${config.age.secrets.secret1.path}
'';
};
in {
imports = [
./install_ssh_host_keys_darwin.nix
../modules/age.nix
];

services.nix-daemon.enable = true;

age.secrets.secret1.file = ../example/secret1.age;

environment.systemPackages = [testScript];
}