1
0
Fork 0
mirror of https://github.com/Mic92/sops-nix.git synced 2024-12-14 11:57:52 +00:00
sops-nix/modules/home-manager/sops.nix
Sirio Balmelli 48afd3264e home-manager/darwin: run sops-nix-user _once_ on login or activation
Correct a bug where 'KeepAlive' in the launchd service
'org.nix-community.home.sops-nix.plist'
re-runs 'sops-nix-user' every few seconds.

This should run:

- once at boot
- once at home-manager profile activation

Signed-off-by: Sirio Balmelli <sirio@b-ad.ch>
2024-02-13 17:36:42 +01:00

270 lines
8.4 KiB
Nix

{ config, lib, pkgs, ... }:
let
cfg = config.sops;
sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets;
secretType = lib.types.submodule ({ config, name, ... }: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
description = ''
Name of the file used in /run/user/*/secrets
'';
};
key = lib.mkOption {
type = lib.types.str;
default = name;
description = ''
Key used to lookup in the sops file.
No tested data structures are supported right now.
This option is ignored if format is binary.
'';
};
path = lib.mkOption {
type = lib.types.str;
default = "${cfg.defaultSymlinkPath}/${name}";
description = ''
Path where secrets are symlinked to.
If the default is kept no other symlink is created.
`%r` is replaced by $XDG_RUNTIME_DIR on linux or `getconf
DARWIN_USER_TEMP_DIR` on darwin.
'';
};
format = lib.mkOption {
type = lib.types.enum [ "yaml" "json" "binary" "ini" "dotenv" ];
default = cfg.defaultSopsFormat;
description = ''
File format used to decrypt the sops secret.
Binary files are written to the target file as is.
'';
};
mode = lib.mkOption {
type = lib.types.str;
default = "0400";
description = ''
Permissions mode of the in octal.
'';
};
sopsFile = lib.mkOption {
type = lib.types.path;
default = cfg.defaultSopsFile;
defaultText = "\${config.sops.defaultSopsFile}";
description = ''
Sops file the secret is loaded from.
'';
};
};
});
manifestFor = suffix: secrets: pkgs.writeTextFile {
name = "manifest${suffix}.json";
text = builtins.toJSON {
secrets = builtins.attrValues secrets;
secretsMountPoint = cfg.defaultSecretsMountPoint;
symlinkPath = cfg.defaultSymlinkPath;
keepGenerations = cfg.keepGenerations;
gnupgHome = cfg.gnupg.home;
sshKeyPaths = cfg.gnupg.sshKeyPaths;
ageKeyFile = cfg.age.keyFile;
ageSshKeyPaths = cfg.age.sshKeyPaths;
userMode = true;
logging = {
keyImport = builtins.elem "keyImport" cfg.log;
secretChanges = builtins.elem "secretChanges" cfg.log;
};
};
checkPhase = ''
${sops-install-secrets}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out"
'';
};
manifest = manifestFor "" cfg.secrets;
script = toString (pkgs.writeShellScript "sops-nix-user" ((lib.optionalString (cfg.gnupg.home != null) ''
export SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg
'')
+ (lib.optionalString cfg.age.generateKey ''
if [[ ! -f '${cfg.age.keyFile}' ]]; then
echo generating machine-specific age key...
mkdir -p $(dirname ${cfg.age.keyFile})
# age-keygen sets 0600 by default, no need to chmod.
${pkgs.age}/bin/age-keygen -o ${cfg.age.keyFile}
fi
'' + ''
${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd '${manifest}'
'')));
in {
options.sops = {
secrets = lib.mkOption {
type = lib.types.attrsOf secretType;
default = {};
description = ''
Secrets to decrypt.
'';
};
defaultSopsFile = lib.mkOption {
type = lib.types.path;
description = ''
Default sops file used for all secrets.
'';
};
defaultSopsFormat = lib.mkOption {
type = lib.types.str;
default = "yaml";
description = ''
Default sops format used for all secrets.
'';
};
validateSopsFiles = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Check all sops files at evaluation time.
This requires sops files to be added to the nix store.
'';
};
defaultSymlinkPath = lib.mkOption {
type = lib.types.str;
default = "%r/secrets";
description = ''
Default place where the latest generation of decrypt secrets
can be found.
'';
};
defaultSecretsMountPoint = lib.mkOption {
type = lib.types.str;
default = "%r/secrets.d";
description = ''
Default place where generations of decrypted secrets are stored.
'';
};
keepGenerations = lib.mkOption {
type = lib.types.ints.unsigned;
default = 1;
description = ''
Number of secrets generations to keep. Setting this to 0 disables pruning.
'';
};
log = lib.mkOption {
type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]);
default = [ "keyImport" "secretChanges" ];
description = "What to log";
};
age = {
keyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/home/someuser/.age-key.txt";
description = ''
Path to age key file used for sops decryption.
'';
};
generateKey = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether or not to generate the age key. If this
option is set to false, the key must already be
present at the specified location.
'';
};
sshKeyPaths = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [];
description = ''
Paths to ssh keys added as age keys during sops description.
'';
};
};
gnupg = {
home = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/home/someuser/.gnupg";
description = ''
Path to gnupg database directory containing the key for decrypting the sops file.
'';
};
sshKeyPaths = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [];
description = ''
Path to ssh keys added as GPG keys during sops description.
This option must be explicitly unset if <literal>config.sops.gnupg.sshKeyPaths</literal> is set.
'';
};
};
};
config = lib.mkIf (cfg.secrets != {}) {
assertions = [{
assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != [];
message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home";
} {
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []);
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
}] ++ lib.optionals cfg.validateSopsFiles (
lib.concatLists (lib.mapAttrsToList (name: secret: [{
assertion = builtins.pathExists secret.sopsFile;
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
} {
assertion =
builtins.isPath secret.sopsFile ||
(builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile);
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false";
}]) cfg.secrets)
);
systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
Unit = {
Description = "sops-nix activation";
};
Service = {
Type = "oneshot";
ExecStart = script;
};
Install.WantedBy = if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ];
};
# Darwin: load secrets once on login
launchd.agents.sops-nix = {
enable = true;
config = {
Program = script;
KeepAlive = false;
RunAtLoad = true;
StandardOutPath = "${config.home.homeDirectory}/Library/Logs/SopsNix/stdout";
StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/SopsNix/stderr";
};
};
# darwin: [re]load secrets on home-manager activation
home.activation = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin {
sops-nix = let
domain-target = "gui/$(id -u ${config.home.username})";
in ''
/bin/launchctl bootout ${domain-target}/org.nix-community.home.sops-nix && true
/bin/launchctl bootstrap ${domain-target} ${config.home.homeDirectory}/Library/LaunchAgents/org.nix-community.home.sops-nix.plist
'';
};
};
}