1
0
Fork 0
mirror of https://github.com/Mic92/sops-nix.git synced 2025-03-05 08:07:16 +00:00

reformat code base with nixfmt

This commit is contained in:
Jörg Thalheim 2024-11-17 12:17:45 +01:00 committed by Jörg Thalheim
parent b05bdb2650
commit 6b85086bcc
24 changed files with 1592 additions and 1159 deletions

View file

@ -1,12 +1,15 @@
{ pkgs ? import <nixpkgs> {} {
, vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=" pkgs ? import <nixpkgs> { },
}: let vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=",
}:
let
sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets {
inherit vendorHash; inherit vendorHash;
}; };
in rec { in
rec {
inherit sops-install-secrets; inherit sops-install-secrets;
sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key {}; sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key { };
default = sops-init-gpg-key; default = sops-init-gpg-key;
sops-pgp-hook = pkgs.lib.warn '' sops-pgp-hook = pkgs.lib.warn ''
@ -22,8 +25,9 @@ in rec {
sops-pgp-hook-test = pkgs.callPackage ./pkgs/sops-pgp-hook-test.nix { sops-pgp-hook-test = pkgs.callPackage ./pkgs/sops-pgp-hook-test.nix {
inherit vendorHash; inherit vendorHash;
}; };
unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix {}; unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { };
} // (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { }
// (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
lint = pkgs.callPackage ./pkgs/lint.nix { lint = pkgs.callPackage ./pkgs/lint.nix {
inherit sops-install-secrets; inherit sops-install-secrets;
}; };

137
flake.nix
View file

@ -2,60 +2,89 @@
description = "Integrates sops into nixos"; description = "Integrates sops into nixos";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.05"; inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.05";
nixConfig.extra-substituters = ["https://cache.thalheim.io"]; nixConfig.extra-substituters = [ "https://cache.thalheim.io" ];
nixConfig.extra-trusted-public-keys = ["cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc="]; nixConfig.extra-trusted-public-keys = [
outputs = { "cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc="
self, ];
nixpkgs, outputs =
nixpkgs-stable {
}: let self,
systems = [ nixpkgs,
"x86_64-linux" nixpkgs-stable,
"x86_64-darwin" }:
"aarch64-darwin" let
"aarch64-linux" systems = [
]; "x86_64-linux"
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); "x86_64-darwin"
suffix-version = version: attrs: nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; "aarch64-darwin"
suffix-stable = suffix-version "-24_05"; "aarch64-linux"
in { ];
overlays.default = final: prev: let forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
localPkgs = import ./default.nix {pkgs = final;}; suffix-version =
in { version: attrs:
inherit (localPkgs) sops-install-secrets sops-init-gpg-key sops-pgp-hook sops-import-keys-hook sops-ssh-to-age; nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs;
# backward compatibility suffix-stable = suffix-version "-24_05";
inherit (prev) ssh-to-pgp; in
}; {
nixosModules = { overlays.default =
sops = ./modules/sops; final: prev:
default = self.nixosModules.sops; let
}; localPkgs = import ./default.nix { pkgs = final; };
homeManagerModules.sops = ./modules/home-manager/sops.nix; in
homeManagerModule = self.homeManagerModules.sops; {
darwinModules = { inherit (localPkgs)
sops = ./modules/nix-darwin; sops-install-secrets
default = self.darwinModules.sops; sops-init-gpg-key
}; sops-pgp-hook
packages = forAllSystems (system: sops-import-keys-hook
import ./default.nix { sops-ssh-to-age
pkgs = import nixpkgs {inherit system;}; ;
}); # backward compatibility
checks = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"] inherit (prev) ssh-to-pgp;
(system: let
tests = self.packages.${system}.sops-install-secrets.tests;
packages-stable = import ./default.nix {
pkgs = import nixpkgs-stable {inherit system;};
}; };
tests-stable = packages-stable.sops-install-secrets.tests; nixosModules = {
in tests // sops = ./modules/sops;
(suffix-stable tests-stable) // default = self.nixosModules.sops;
(suffix-stable packages-stable)); };
homeManagerModules.sops = ./modules/home-manager/sops.nix;
homeManagerModule = self.homeManagerModules.sops;
darwinModules = {
sops = ./modules/nix-darwin;
default = self.darwinModules.sops;
};
packages = forAllSystems (
system:
import ./default.nix {
pkgs = import nixpkgs { inherit system; };
}
);
checks =
nixpkgs.lib.genAttrs
[
"x86_64-linux"
"aarch64-linux"
]
(
system:
let
tests = self.packages.${system}.sops-install-secrets.tests;
packages-stable = import ./default.nix {
pkgs = import nixpkgs-stable { inherit system; };
};
tests-stable = packages-stable.sops-install-secrets.tests;
in
tests // (suffix-stable tests-stable) // (suffix-stable packages-stable)
);
devShells = forAllSystems (system: let devShells = forAllSystems (
pkgs = nixpkgs.legacyPackages.${system}; system:
in { let
unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix {}; pkgs = nixpkgs.legacyPackages.${system};
default = pkgs.callPackage ./shell.nix {}; in
}); {
}; unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { };
default = pkgs.callPackage ./shell.nix { };
}
);
};
} }

View file

@ -1,67 +1,81 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.sops; cfg = config.sops;
sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets; sops-install-secrets = (pkgs.callPackage ../.. { }).sops-install-secrets;
secretType = lib.types.submodule ({ name, ... }: { secretType = lib.types.submodule (
options = { { name, ... }:
name = lib.mkOption { {
type = lib.types.str; options = {
default = name; name = lib.mkOption {
description = '' type = lib.types.str;
Name of the file used in /run/user/*/secrets default = name;
''; description = ''
}; Name of the file used in /run/user/*/secrets
'';
};
key = lib.mkOption { key = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else name; default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else name;
description = '' description = ''
Key used to lookup in the sops file. Key used to lookup in the sops file.
No tested data structures are supported right now. No tested data structures are supported right now.
This option is ignored if format is binary. This option is ignored if format is binary.
"" means whole file. "" means whole file.
''; '';
}; };
path = lib.mkOption { path = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "${cfg.defaultSymlinkPath}/${name}"; default = "${cfg.defaultSymlinkPath}/${name}";
description = '' description = ''
Path where secrets are symlinked to. Path where secrets are symlinked to.
If the default is kept no other symlink is created. If the default is kept no other symlink is created.
`%r` is replaced by $XDG_RUNTIME_DIR on linux or `getconf `%r` is replaced by $XDG_RUNTIME_DIR on linux or `getconf
DARWIN_USER_TEMP_DIR` on darwin. DARWIN_USER_TEMP_DIR` on darwin.
''; '';
}; };
format = lib.mkOption { format = lib.mkOption {
type = lib.types.enum [ "yaml" "json" "binary" "ini" "dotenv" ]; type = lib.types.enum [
default = cfg.defaultSopsFormat; "yaml"
description = '' "json"
File format used to decrypt the sops secret. "binary"
Binary files are written to the target file as is. "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 { mode = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "0400"; default = "0400";
description = '' description = ''
Permissions mode of the in octal. Permissions mode of the in octal.
''; '';
}; };
sopsFile = lib.mkOption { sopsFile = lib.mkOption {
type = lib.types.path; type = lib.types.path;
default = cfg.defaultSopsFile; default = cfg.defaultSopsFile;
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
description = '' description = ''
Sops file the secret is loaded from. Sops file the secret is loaded from.
''; '';
};
}; };
}; }
}); );
pathNotInStore = lib.mkOptionType { pathNotInStore = lib.mkOptionType {
name = "pathNotInStore"; name = "pathNotInStore";
@ -71,44 +85,54 @@ let
merge = lib.mergeEqualOption; merge = lib.mergeEqualOption;
}; };
manifestFor = suffix: secrets: templates: pkgs.writeTextFile { manifestFor =
name = "manifest${suffix}.json"; suffix: secrets: templates:
text = builtins.toJSON { pkgs.writeTextFile {
secrets = builtins.attrValues secrets; name = "manifest${suffix}.json";
templates = builtins.attrValues templates; text = builtins.toJSON {
secretsMountPoint = cfg.defaultSecretsMountPoint; secrets = builtins.attrValues secrets;
symlinkPath = cfg.defaultSymlinkPath; templates = builtins.attrValues templates;
keepGenerations = cfg.keepGenerations; secretsMountPoint = cfg.defaultSecretsMountPoint;
gnupgHome = cfg.gnupg.home; symlinkPath = cfg.defaultSymlinkPath;
sshKeyPaths = cfg.gnupg.sshKeyPaths; keepGenerations = cfg.keepGenerations;
ageKeyFile = cfg.age.keyFile; gnupgHome = cfg.gnupg.home;
ageSshKeyPaths = cfg.age.sshKeyPaths; sshKeyPaths = cfg.gnupg.sshKeyPaths;
userMode = true; ageKeyFile = cfg.age.keyFile;
logging = { ageSshKeyPaths = cfg.age.sshKeyPaths;
keyImport = builtins.elem "keyImport" cfg.log; userMode = true;
secretChanges = builtins.elem "secretChanges" cfg.log; 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"
'';
}; };
checkPhase = ''
${sops-install-secrets}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out"
'';
};
manifest = manifestFor "" cfg.secrets cfg.templates; manifest = manifestFor "" cfg.secrets cfg.templates;
escapedAgeKeyFile = lib.escapeShellArg cfg.age.keyFile; escapedAgeKeyFile = lib.escapeShellArg cfg.age.keyFile;
script = toString (pkgs.writeShellScript "sops-nix-user" (lib.optionalString cfg.age.generateKey '' script = toString (
if [[ ! -f ${escapedAgeKeyFile} ]]; then pkgs.writeShellScript "sops-nix-user" (
echo generating machine-specific age key... lib.optionalString cfg.age.generateKey ''
${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile}) if [[ ! -f ${escapedAgeKeyFile} ]]; then
# age-keygen sets 0600 by default, no need to chmod. echo generating machine-specific age key...
${pkgs.age}/bin/age-keygen -o ${escapedAgeKeyFile} ${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile})
fi # age-keygen sets 0600 by default, no need to chmod.
'' + '' ${pkgs.age}/bin/age-keygen -o ${escapedAgeKeyFile}
${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest} fi
'')); ''
in { + ''
${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest}
''
)
);
in
{
imports = [ imports = [
./templates.nix ./templates.nix
]; ];
@ -116,7 +140,7 @@ in {
options.sops = { options.sops = {
secrets = lib.mkOption { secrets = lib.mkOption {
type = lib.types.attrsOf secretType; type = lib.types.attrsOf secretType;
default = {}; default = { };
description = '' description = ''
Secrets to decrypt. Secrets to decrypt.
''; '';
@ -182,14 +206,22 @@ in {
}; };
log = lib.mkOption { log = lib.mkOption {
type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); type = lib.types.listOf (
default = [ "keyImport" "secretChanges" ]; lib.types.enum [
"keyImport"
"secretChanges"
]
);
default = [
"keyImport"
"secretChanges"
];
description = "What to log"; description = "What to log";
}; };
environment = lib.mkOption { environment = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path);
default = {}; default = { };
description = '' description = ''
Environment variables to set before calling sops-install-secrets. Environment variables to set before calling sops-install-secrets.
@ -219,7 +251,7 @@ in {
sshKeyPaths = lib.mkOption { sshKeyPaths = lib.mkOption {
type = lib.types.listOf lib.types.path; type = lib.types.listOf lib.types.path;
default = []; default = [ ];
description = '' description = ''
Paths to ssh keys added as age keys during sops description. Paths to ssh keys added as age keys during sops description.
''; '';
@ -251,7 +283,7 @@ in {
sshKeyPaths = lib.mkOption { sshKeyPaths = lib.mkOption {
type = lib.types.listOf lib.types.path; type = lib.types.listOf lib.types.path;
default = []; default = [ ];
description = '' description = ''
Path to ssh keys added as GPG keys during sops 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. This option must be explicitly unset if <literal>config.sops.gnupg.sshKeyPaths</literal> is set.
@ -260,37 +292,52 @@ in {
}; };
}; };
config = lib.mkIf (cfg.secrets != {}) { config = lib.mkIf (cfg.secrets != { }) {
assertions = [{ assertions =
assertion = [
cfg.gnupg.home != null || {
cfg.gnupg.sshKeyPaths != [] || assertion =
cfg.gnupg.qubes-split-gpg.enable == true || cfg.gnupg.home != null
cfg.age.keyFile != null || || cfg.gnupg.sshKeyPaths != [ ]
cfg.age.sshKeyPaths != []; || cfg.gnupg.qubes-split-gpg.enable == true
message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home or sops.gnupg.qubes-split-gpg.enable"; || cfg.age.keyFile != null
} { || cfg.age.sshKeyPaths != [ ];
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []) && message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home or sops.gnupg.qubes-split-gpg.enable";
!(cfg.gnupg.home != null && cfg.gnupg.qubes-split-gpg.enable == true) && }
!(cfg.gnupg.sshKeyPaths != [ ] && cfg.gnupg.qubes-split-gpg.enable == true); {
message = "Exactly one of sops.gnupg.home, sops.gnupg.qubes-split-gpg.enable and sops.gnupg.sshKeyPaths must be set"; assertion =
} { !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ])
assertion = cfg.gnupg.qubes-split-gpg.enable == false || && !(cfg.gnupg.home != null && cfg.gnupg.qubes-split-gpg.enable == true)
(cfg.gnupg.qubes-split-gpg.enable == true && && !(cfg.gnupg.sshKeyPaths != [ ] && cfg.gnupg.qubes-split-gpg.enable == true);
cfg.gnupg.qubes-split-gpg.domain != null && message = "Exactly one of sops.gnupg.home, sops.gnupg.qubes-split-gpg.enable and sops.gnupg.sshKeyPaths must be set";
cfg.gnupg.qubes-split-gpg.domain != ""); }
message = "sops.gnupg.qubes-split-gpg.domain is required when sops.gnupg.qubes-split-gpg.enable is set to true"; {
}] ++ lib.optionals cfg.validateSopsFiles ( assertion =
lib.concatLists (lib.mapAttrsToList (name: secret: [{ cfg.gnupg.qubes-split-gpg.enable == false
assertion = builtins.pathExists secret.sopsFile; || (
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; cfg.gnupg.qubes-split-gpg.enable == true
} { && cfg.gnupg.qubes-split-gpg.domain != null
assertion = && cfg.gnupg.qubes-split-gpg.domain != ""
builtins.isPath secret.sopsFile || );
(builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); message = "sops.gnupg.qubes-split-gpg.domain is required when sops.gnupg.qubes-split-gpg.enable is set to true";
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; }
}]) cfg.secrets) ]
); ++ 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
)
);
home.sessionVariables = lib.mkIf cfg.gnupg.qubes-split-gpg.enable { home.sessionVariables = lib.mkIf cfg.gnupg.qubes-split-gpg.enable {
# TODO: Add this package to nixpkgs and use it from the store # TODO: Add this package to nixpkgs and use it from the store
@ -300,11 +347,17 @@ in {
sops.environment = { sops.environment = {
SOPS_GPG_EXEC = lib.mkMerge [ SOPS_GPG_EXEC = lib.mkMerge [
(lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg")) (lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) (
(lib.mkIf cfg.gnupg.qubes-split-gpg.enable (lib.mkDefault config.home.sessionVariables.SOPS_GPG_EXEC)) lib.mkDefault "${pkgs.gnupg}/bin/gpg"
))
(lib.mkIf cfg.gnupg.qubes-split-gpg.enable (
lib.mkDefault config.home.sessionVariables.SOPS_GPG_EXEC
))
]; ];
QUBES_GPG_DOMAIN = lib.mkIf cfg.gnupg.qubes-split-gpg.enable (lib.mkDefault cfg.gnupg.qubes-split-gpg.domain); QUBES_GPG_DOMAIN = lib.mkIf cfg.gnupg.qubes-split-gpg.enable (
lib.mkDefault cfg.gnupg.qubes-split-gpg.domain
);
}; };
systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux { systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
@ -313,10 +366,13 @@ in {
}; };
Service = { Service = {
Type = "oneshot"; Type = "oneshot";
Environment = builtins.concatStringsSep " " (lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment); Environment = builtins.concatStringsSep " " (
lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment
);
ExecStart = script; ExecStart = script;
}; };
Install.WantedBy = if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ]; Install.WantedBy =
if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ];
}; };
# Darwin: load secrets once on login # Darwin: load secrets once on login
@ -333,28 +389,36 @@ in {
}; };
# [re]load secrets on home-manager activation # [re]load secrets on home-manager activation
home.activation = let home.activation =
darwin = let let
domain-target = "gui/$(id -u ${config.home.username})"; darwin =
in '' let
/bin/launchctl bootout ${domain-target}/org.nix-community.home.sops-nix && true domain-target = "gui/$(id -u ${config.home.username})";
/bin/launchctl bootstrap ${domain-target} ${config.home.homeDirectory}/Library/LaunchAgents/org.nix-community.home.sops-nix.plist 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
'';
linux = let systemctl = config.systemd.user.systemctlPath; in '' linux =
systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true) let
systemctl = config.systemd.user.systemctlPath;
in
''
systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true)
if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then
${systemctl} restart --user sops-nix ${systemctl} restart --user sops-nix
else else
echo "User systemd daemon not running. Probably executed on boot where no manual start/reload is needed." echo "User systemd daemon not running. Probably executed on boot where no manual start/reload is needed."
fi fi
unset systemdStatus unset systemdStatus
''; '';
in { in
sops-nix = if pkgs.stdenv.isLinux then linux else darwin; {
}; sops-nix = if pkgs.stdenv.isLinux then linux else darwin;
};
}; };
} }

View file

@ -1,91 +1,106 @@
{ config, pkgs, lib, options, ... }: {
config,
pkgs,
lib,
options,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
mkDefault mkDefault
mapAttrs mapAttrs
types types
; ;
in { in
{
options.sops = { options.sops = {
templates = mkOption { templates = mkOption {
description = "Templates for secret files"; description = "Templates for secret files";
type = types.attrsOf (types.submodule ({ config, ... }: { type = types.attrsOf (
options = { types.submodule (
name = mkOption { { config, ... }:
type = types.singleLineStr; {
default = config._module.args.name; options = {
description = '' name = mkOption {
Name of the file used in /run/secrets/rendered type = types.singleLineStr;
''; default = config._module.args.name;
}; description = ''
path = mkOption { Name of the file used in /run/secrets/rendered
description = "Path where the rendered file will be placed"; '';
type = types.singleLineStr; };
# Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` path = mkOption {
default = "${config.xdg.configHome}/sops-nix/secrets/rendered/${config.name}"; description = "Path where the rendered file will be placed";
}; type = types.singleLineStr;
content = mkOption { # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go`
type = types.lines; default = "${config.xdg.configHome}/sops-nix/secrets/rendered/${config.name}";
default = ""; };
description = '' content = mkOption {
Content of the file type = types.lines;
''; default = "";
}; description = ''
mode = mkOption { Content of the file
type = types.singleLineStr; '';
default = "0400"; };
description = '' mode = mkOption {
Permissions mode of the rendered secret file in octal. type = types.singleLineStr;
''; default = "0400";
}; description = ''
file = mkOption { Permissions mode of the rendered secret file in octal.
type = types.path; '';
default = pkgs.writeText config.name config.content; };
defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; file = mkOption {
example = "./configuration-template.conf"; type = types.path;
description = '' default = pkgs.writeText config.name config.content;
File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored. defaultText = lib.literalExpression ''pkgs.writeText config.name config.content'';
''; example = "./configuration-template.conf";
}; description = ''
restartUnits = lib.mkOption { File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored.
type = lib.types.listOf lib.types.str; '';
default = [ ]; };
example = [ "sshd.service" ]; restartUnits = lib.mkOption {
description = '' type = lib.types.listOf lib.types.str;
Names of units that should be restarted when the rendered template changes. default = [ ];
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />. example = [ "sshd.service" ];
''; description = ''
}; Names of units that should be restarted when the rendered template changes.
reloadUnits = lib.mkOption { This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
type = lib.types.listOf lib.types.str; '';
default = [ ]; };
example = [ "sshd.service" ]; reloadUnits = lib.mkOption {
description = '' type = lib.types.listOf lib.types.str;
Names of units that should be reloaded when the rendered template changes. default = [ ];
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />. example = [ "sshd.service" ];
''; description = ''
}; Names of units that should be reloaded when the rendered template changes.
}; This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
})); '';
};
};
}
)
);
default = { }; default = { };
}; };
placeholder = mkOption { placeholder = mkOption {
type = types.attrsOf (types.mkOptionType { type = types.attrsOf (
name = "coercibleToString"; types.mkOptionType {
description = "value that can be coerced to string"; name = "coercibleToString";
check = lib.strings.isConvertibleWithToString; description = "value that can be coerced to string";
merge = lib.mergeEqualOption; check = lib.strings.isConvertibleWithToString;
}); merge = lib.mergeEqualOption;
}
);
default = { }; default = { };
visible = false; visible = false;
}; };
}; };
config = lib.optionalAttrs (options ? sops.secrets) config = lib.optionalAttrs (options ? sops.secrets) (
(lib.mkIf (config.sops.templates != { }) { lib.mkIf (config.sops.templates != { }) {
sops.placeholder = mapAttrs sops.placeholder = mapAttrs (
(name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>") name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>"
config.sops.secrets; ) config.sops.secrets;
}); }
);
} }

View file

@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.sops; cfg = config.sops;
@ -7,7 +12,7 @@ let
inherit cfg; inherit cfg;
inherit (pkgs) writeTextFile; inherit (pkgs) writeTextFile;
}; };
manifest = manifestFor "" regularSecrets regularTemplates {}; manifest = manifestFor "" regularSecrets regularTemplates { };
# Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.) # Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.)
regularTemplates = cfg.templates; regularTemplates = cfg.templates;
@ -25,138 +30,165 @@ let
withEnvironment = import ./with-environment.nix { withEnvironment = import ./with-environment.nix {
inherit cfg lib; inherit cfg lib;
}; };
secretType = lib.types.submodule ({ config, ... }: { secretType = lib.types.submodule (
config = { { config, ... }:
sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; {
sopsFileHash = lib.mkOptionDefault (lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); config = {
}; sopsFile = lib.mkOptionDefault cfg.defaultSopsFile;
options = { sopsFileHash = lib.mkOptionDefault (
name = lib.mkOption { lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"
type = lib.types.str; );
default = config._module.args.name;
description = ''
Name of the file used in /run/secrets
'';
}; };
key = lib.mkOption { options = {
type = lib.types.str; name = lib.mkOption {
default = config._module.args.name; type = lib.types.str;
description = '' default = config._module.args.name;
Key used to lookup in the sops file. description = ''
No tested data structures are supported right now. Name of the file used in /run/secrets
This option is ignored if format is binary. '';
''; };
key = lib.mkOption {
type = lib.types.str;
default = config._module.args.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 =
if config.neededForUsers then
"/run/secrets-for-users/${config.name}"
else
"/run/secrets/${config.name}";
defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise.";
description = ''
Path where secrets are symlinked to.
If the default is kept no symlink is created.
'';
};
format = lib.mkOption {
type = lib.types.enum [
"yaml"
"json"
"binary"
"dotenv"
"ini"
];
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.
'';
};
owner = lib.mkOption {
type = with lib.types; nullOr str;
default = "root";
description = ''
User of the file. Can only be set if uid is 0.
'';
};
uid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist.
'';
};
group = lib.mkOption {
type = with lib.types; nullOr str;
default = "staff";
defaultText = "staff";
description = ''
Group of the file. Can only be set if gid is 0.
'';
};
gid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist.
'';
};
sopsFile = lib.mkOption {
type = lib.types.path;
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
description = ''
Sops file the secret is loaded from.
'';
};
sopsFileHash = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
Hash of the sops file.
'';
};
neededForUsers = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
**Warning** This option doesn't have any effect on macOS, as nix-darwin cannot manage user passwords on macOS.
This can be used to retrieve user's passwords from sops-nix.
Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
'';
};
}; };
path = lib.mkOption { }
type = lib.types.str; );
default = if config.neededForUsers then "/run/secrets-for-users/${config.name}" else "/run/secrets/${config.name}";
defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise.";
description = ''
Path where secrets are symlinked to.
If the default is kept no symlink is created.
'';
};
format = lib.mkOption {
type = lib.types.enum ["yaml" "json" "binary" "dotenv" "ini"];
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.
'';
};
owner = lib.mkOption {
type = with lib.types; nullOr str;
default = "root";
description = ''
User of the file. Can only be set if uid is 0.
'';
};
uid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist.
'';
};
group = lib.mkOption {
type = with lib.types; nullOr str;
default = "staff";
defaultText = "staff";
description = ''
Group of the file. Can only be set if gid is 0.
'';
};
gid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist.
'';
};
sopsFile = lib.mkOption {
type = lib.types.path;
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
description = ''
Sops file the secret is loaded from.
'';
};
sopsFileHash = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
Hash of the sops file.
'';
};
neededForUsers = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
**Warning** This option doesn't have any effect on macOS, as nix-darwin cannot manage user passwords on macOS.
This can be used to retrieve user's passwords from sops-nix.
Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
'';
};
};
});
darwinSSHKeys = [{ darwinSSHKeys = [
type = "rsa"; {
path = "/etc/ssh/ssh_host_rsa_key"; type = "rsa";
} { path = "/etc/ssh/ssh_host_rsa_key";
type = "ed25519"; }
path = "/etc/ssh/ssh_host_ed25519_key"; {
}]; type = "ed25519";
path = "/etc/ssh/ssh_host_ed25519_key";
}
];
escapedKeyFile = lib.escapeShellArg cfg.age.keyFile; escapedKeyFile = lib.escapeShellArg cfg.age.keyFile;
# Skip ssh keys deployed with sops to avoid a catch 22 # Skip ssh keys deployed with sops to avoid a catch 22
defaultImportKeys = algo: defaultImportKeys =
map (e: e.path) (lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) darwinSSHKeys); algo:
map (e: e.path) (
lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) darwinSSHKeys
);
installScript = '' installScript = ''
${if cfg.age.generateKey then '' ${
if [[ ! -f ${escapedKeyFile} ]]; then if cfg.age.generateKey then
echo generating machine-specific age key... ''
mkdir -p $(dirname ${escapedKeyFile}) if [[ ! -f ${escapedKeyFile} ]]; then
# age-keygen sets 0600 by default, no need to chmod. echo generating machine-specific age key...
${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} mkdir -p $(dirname ${escapedKeyFile})
fi # age-keygen sets 0600 by default, no need to chmod.
'' else ""} ${pkgs.age}/bin/age-keygen -o ${escapedKeyFile}
fi
''
else
""
}
echo "Setting up secrets..." echo "Setting up secrets..."
${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"} ${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"}
''; '';
in { in
{
options.sops = { options.sops = {
secrets = lib.mkOption { secrets = lib.mkOption {
type = lib.types.attrsOf secretType; type = lib.types.attrsOf secretType;
default = {}; default = { };
description = '' description = ''
Path where the latest secrets are mounted to. Path where the latest secrets are mounted to.
''; '';
@ -195,14 +227,22 @@ in {
}; };
log = lib.mkOption { log = lib.mkOption {
type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); type = lib.types.listOf (
default = [ "keyImport" "secretChanges" ]; lib.types.enum [
"keyImport"
"secretChanges"
]
);
default = [
"keyImport"
"secretChanges"
];
description = "What to log"; description = "What to log";
}; };
environment = lib.mkOption { environment = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path);
default = {}; default = { };
description = '' description = ''
Environment variables to set before calling sops-install-secrets. Environment variables to set before calling sops-install-secrets.
@ -217,7 +257,7 @@ in {
package = lib.mkOption { package = lib.mkOption {
type = lib.types.package; type = lib.types.package;
default = (pkgs.callPackage ../.. {}).sops-install-secrets; default = (pkgs.callPackage ../.. { }).sops-install-secrets;
defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets"; defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets";
description = '' description = ''
sops-install-secrets package to use. sops-install-secrets package to use.
@ -227,9 +267,10 @@ in {
validationPackage = lib.mkOption { validationPackage = lib.mkOption {
type = lib.types.package; type = lib.types.package;
default = default =
if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then
then sops-install-secrets sops-install-secrets
else (pkgs.pkgsBuildHost.callPackage ../.. {}).sops-install-secrets; else
(pkgs.pkgsBuildHost.callPackage ../.. { }).sops-install-secrets;
defaultText = lib.literalExpression "config.sops.package"; defaultText = lib.literalExpression "config.sops.package";
description = '' description = ''
@ -296,30 +337,46 @@ in {
]; ];
config = lib.mkMerge [ config = lib.mkMerge [
(lib.mkIf (cfg.secrets != {}) { (lib.mkIf (cfg.secrets != { }) {
assertions = [{ 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 =
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []); cfg.gnupg.home != null
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; || cfg.gnupg.sshKeyPaths != [ ]
}] ++ lib.optionals cfg.validateSopsFiles ( || cfg.age.keyFile != null
lib.concatLists (lib.mapAttrsToList (name: secret: [{ || cfg.age.sshKeyPaths != [ ];
assertion = builtins.pathExists secret.sopsFile; message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home";
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; }
} { {
assertion = assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ]);
builtins.isPath secret.sopsFile || message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
(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"; ]
} { ++ lib.optionals cfg.validateSopsFiles (
assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; lib.concatLists (
message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; lib.mapAttrsToList (name: secret: [
} { {
assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; assertion = builtins.pathExists secret.sopsFile;
message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
}]) cfg.secrets) }
); {
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";
}
{
assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null;
message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set";
}
{
assertion = secret.gid != null && secret.gid != 0 -> secret.group == null;
message = "In ${secret.name} exactly one of sops.group and sops.gid must be set";
}
]) cfg.secrets
)
);
system.build.sops-nix-manifest = manifest; system.build.sops-nix-manifest = manifest;
system.activationScripts = { system.activationScripts = {
@ -336,7 +393,9 @@ in {
}) })
{ {
sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) (
lib.mkDefault "${pkgs.gnupg}/bin/gpg"
);
} }
]; ];
} }

View file

@ -4,26 +4,31 @@ suffix: secrets: extraJson:
writeTextFile { writeTextFile {
name = "manifest${suffix}.json"; name = "manifest${suffix}.json";
text = builtins.toJSON ({ text = builtins.toJSON (
secrets = builtins.attrValues secrets; {
# Does this need to be configurable? secrets = builtins.attrValues secrets;
secretsMountPoint = "/run/secrets.d"; # Does this need to be configurable?
symlinkPath = "/run/secrets"; secretsMountPoint = "/run/secrets.d";
keepGenerations = cfg.keepGenerations; symlinkPath = "/run/secrets";
gnupgHome = cfg.gnupg.home; keepGenerations = cfg.keepGenerations;
sshKeyPaths = cfg.gnupg.sshKeyPaths; gnupgHome = cfg.gnupg.home;
ageKeyFile = cfg.age.keyFile; sshKeyPaths = cfg.gnupg.sshKeyPaths;
ageSshKeyPaths = cfg.age.sshKeyPaths; ageKeyFile = cfg.age.keyFile;
useTmpfs = false; ageSshKeyPaths = cfg.age.sshKeyPaths;
templates = cfg.templates; useTmpfs = false;
placeholderBySecretName = cfg.placeholder; templates = cfg.templates;
userMode = false; placeholderBySecretName = cfg.placeholder;
logging = { userMode = false;
keyImport = builtins.elem "keyImport" cfg.log; logging = {
secretChanges = builtins.elem "secretChanges" cfg.log; keyImport = builtins.elem "keyImport" cfg.log;
}; secretChanges = builtins.elem "secretChanges" cfg.log;
} // extraJson); };
}
// extraJson
);
checkPhase = '' checkPhase = ''
${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out" ${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${
if cfg.validateSopsFiles then "sopsfile" else "manifest"
} "$out"
''; '';
} }

View file

@ -1,8 +1,14 @@
{ lib, options, config, pkgs, ... }: {
lib,
options,
config,
pkgs,
...
}:
let let
cfg = config.sops; cfg = config.sops;
secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets;
templatesForUsers = {}; # We do not currently support `neededForUsers` for templates. templatesForUsers = { }; # We do not currently support `neededForUsers` for templates.
manifestFor = pkgs.callPackage ../manifest-for.nix { manifestFor = pkgs.callPackage ../manifest-for.nix {
inherit cfg; inherit cfg;
inherit (pkgs) writeTextFile; inherit (pkgs) writeTextFile;
@ -22,16 +28,21 @@ let
in in
{ {
assertions = [{ assertions = [
assertion = (lib.filterAttrs (_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")) secretsForUsers) == { }; {
message = "neededForUsers cannot be used for secrets that are not root-owned"; assertion =
}]; (lib.filterAttrs (
_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")
) secretsForUsers) == { };
message = "neededForUsers cannot be used for secrets that are not root-owned";
}
];
system.activationScripts = lib.mkIf (secretsForUsers != []) { system.activationScripts = lib.mkIf (secretsForUsers != [ ]) {
postActivation.text = lib.mkAfter installScript; postActivation.text = lib.mkAfter installScript;
}; };
launchd.daemons.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != []) { launchd.daemons.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != [ ]) {
command = installScript; command = installScript;
serviceConfig = { serviceConfig = {
RunAtLoad = true; RunAtLoad = true;

View file

@ -1,87 +1,102 @@
{ config, pkgs, lib, options, ... }: {
config,
pkgs,
lib,
options,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
mkDefault mkDefault
mapAttrs mapAttrs
types types
; ;
in { in
{
options.sops = { options.sops = {
templates = mkOption { templates = mkOption {
description = "Templates for secret files"; description = "Templates for secret files";
type = types.attrsOf (types.submodule ({ config, ... }: { type = types.attrsOf (
options = { types.submodule (
name = mkOption { { config, ... }:
type = types.singleLineStr; {
default = config._module.args.name; options = {
description = '' name = mkOption {
Name of the file used in /run/secrets/rendered type = types.singleLineStr;
''; default = config._module.args.name;
}; description = ''
path = mkOption { Name of the file used in /run/secrets/rendered
description = "Path where the rendered file will be placed"; '';
type = types.singleLineStr; };
default = "/run/secrets/rendered/${config.name}"; path = mkOption {
}; description = "Path where the rendered file will be placed";
content = mkOption { type = types.singleLineStr;
type = types.lines; default = "/run/secrets/rendered/${config.name}";
default = ""; };
description = '' content = mkOption {
Content of the file type = types.lines;
''; default = "";
}; description = ''
mode = mkOption { Content of the file
type = types.singleLineStr; '';
default = "0400"; };
description = '' mode = mkOption {
Permissions mode of the rendered secret file in octal. type = types.singleLineStr;
''; default = "0400";
}; description = ''
owner = mkOption { Permissions mode of the rendered secret file in octal.
type = types.singleLineStr; '';
default = "root"; };
description = '' owner = mkOption {
User of the file. type = types.singleLineStr;
''; default = "root";
}; description = ''
group = mkOption { User of the file.
type = types.singleLineStr; '';
default = "staff"; };
defaultText = "staff"; group = mkOption {
description = '' type = types.singleLineStr;
Group of the file. Default on darwin in staff. default = "staff";
''; defaultText = "staff";
}; description = ''
file = mkOption { Group of the file. Default on darwin in staff.
type = types.path; '';
default = pkgs.writeText config.name config.content; };
defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; file = mkOption {
example = "./configuration-template.conf"; type = types.path;
description = '' default = pkgs.writeText config.name config.content;
File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored. defaultText = lib.literalExpression ''pkgs.writeText config.name config.content'';
''; example = "./configuration-template.conf";
}; description = ''
}; File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored.
})); '';
};
};
}
)
);
default = { }; default = { };
}; };
placeholder = mkOption { placeholder = mkOption {
type = types.attrsOf (types.mkOptionType { type = types.attrsOf (
name = "coercibleToString"; types.mkOptionType {
description = "value that can be coerced to string"; name = "coercibleToString";
check = lib.strings.isConvertibleWithToString; description = "value that can be coerced to string";
merge = lib.mergeEqualOption; check = lib.strings.isConvertibleWithToString;
}); merge = lib.mergeEqualOption;
}
);
default = { }; default = { };
visible = false; visible = false;
}; };
}; };
config = lib.optionalAttrs (options ? sops.secrets) config = lib.optionalAttrs (options ? sops.secrets) (
(lib.mkIf (config.sops.templates != { }) { lib.mkIf (config.sops.templates != { }) {
sops.placeholder = mapAttrs sops.placeholder = mapAttrs (
(name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>") name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>"
config.sops.secrets; ) config.sops.secrets;
}); }
);
} }

View file

@ -2,12 +2,13 @@
sopsCall: sopsCall:
if cfg.environment == {} then if cfg.environment == { } then
sopsCall sopsCall
else '' else
( ''
# shellcheck disable=SC2030,SC2031 (
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} # shellcheck disable=SC2030,SC2031
${sopsCall} ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)}
) ${sopsCall}
'' )
''

View file

@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }: {
config,
options,
lib,
pkgs,
...
}:
let let
cfg = config.sops; cfg = config.sops;
@ -8,7 +14,7 @@ let
inherit cfg; inherit cfg;
inherit (pkgs) writeTextFile; inherit (pkgs) writeTextFile;
}; };
manifest = manifestFor "" regularSecrets regularTemplates {}; manifest = manifestFor "" regularSecrets regularTemplates { };
pathNotInStore = lib.mkOptionType { pathNotInStore = lib.mkOptionType {
name = "pathNotInStore"; name = "pathNotInStore";
@ -23,143 +29,165 @@ let
# Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.) # Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.)
regularTemplates = cfg.templates; regularTemplates = cfg.templates;
useSystemdActivation = (options.systemd ? sysusers && config.systemd.sysusers.enable) || useSystemdActivation =
(options.services ? userborn && config.services.userborn.enable); (options.systemd ? sysusers && config.systemd.sysusers.enable)
|| (options.services ? userborn && config.services.userborn.enable);
withEnvironment = import ./with-environment.nix { withEnvironment = import ./with-environment.nix {
inherit cfg lib; inherit cfg lib;
}; };
secretType = lib.types.submodule ({ config, ... }: { secretType = lib.types.submodule (
config = { { config, ... }:
sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; {
sopsFileHash = lib.mkOptionDefault (lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); config = {
}; sopsFile = lib.mkOptionDefault cfg.defaultSopsFile;
options = { sopsFileHash = lib.mkOptionDefault (
name = lib.mkOption { lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"
type = lib.types.str; );
default = config._module.args.name;
description = ''
Name of the file used in /run/secrets
'';
}; };
key = lib.mkOption { options = {
type = lib.types.str; name = lib.mkOption {
default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else config._module.args.name; type = lib.types.str;
description = '' default = config._module.args.name;
Key used to lookup in the sops file. description = ''
No tested data structures are supported right now. Name of the file used in /run/secrets
This option is ignored if format is binary. '';
"" means whole file. };
''; key = lib.mkOption {
type = lib.types.str;
default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else config._module.args.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.
"" means whole file.
'';
};
path = lib.mkOption {
type = lib.types.str;
default =
if config.neededForUsers then
"/run/secrets-for-users/${config.name}"
else
"/run/secrets/${config.name}";
defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise.";
description = ''
Path where secrets are symlinked to.
If the default is kept no symlink is created.
'';
};
format = lib.mkOption {
type = lib.types.enum [
"yaml"
"json"
"binary"
"dotenv"
"ini"
];
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.
'';
};
owner = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
User of the file. Can only be set if uid is 0.
'';
};
uid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist.
'';
};
group = lib.mkOption {
type = with lib.types; nullOr str;
default = if config.owner != null then users.${config.owner}.group else null;
defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`";
description = ''
Group of the file. Can only be set if gid is 0.
'';
};
gid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist.
'';
};
sopsFile = lib.mkOption {
type = lib.types.path;
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
description = ''
Sops file the secret is loaded from.
'';
};
sopsFileHash = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
Hash of the sops file, useful in <xref linkend="opt-systemd.services._name_.restartTriggers" />.
'';
};
restartUnits = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "sshd.service" ];
description = ''
Names of units that should be restarted when this secret changes.
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
'';
};
reloadUnits = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "sshd.service" ];
description = ''
Names of units that should be reloaded when this secret changes.
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
'';
};
neededForUsers = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enabling this option causes the secret to be decrypted before users and groups are created.
This can be used to retrieve user's passwords from sops-nix.
Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
'';
};
}; };
path = lib.mkOption { }
type = lib.types.str; );
default = if config.neededForUsers then "/run/secrets-for-users/${config.name}" else "/run/secrets/${config.name}";
defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise.";
description = ''
Path where secrets are symlinked to.
If the default is kept no symlink is created.
'';
};
format = lib.mkOption {
type = lib.types.enum ["yaml" "json" "binary" "dotenv" "ini"];
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.
'';
};
owner = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
User of the file. Can only be set if uid is 0.
'';
};
uid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist.
'';
};
group = lib.mkOption {
type = with lib.types; nullOr str;
default = if config.owner != null then users.${config.owner}.group else null;
defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`";
description = ''
Group of the file. Can only be set if gid is 0.
'';
};
gid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist.
'';
};
sopsFile = lib.mkOption {
type = lib.types.path;
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
description = ''
Sops file the secret is loaded from.
'';
};
sopsFileHash = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
Hash of the sops file, useful in <xref linkend="opt-systemd.services._name_.restartTriggers" />.
'';
};
restartUnits = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "sshd.service" ];
description = ''
Names of units that should be restarted when this secret changes.
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
'';
};
reloadUnits = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "sshd.service" ];
description = ''
Names of units that should be reloaded when this secret changes.
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
'';
};
neededForUsers = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enabling this option causes the secret to be decrypted before users and groups are created.
This can be used to retrieve user's passwords from sops-nix.
Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
'';
};
};
});
# Skip ssh keys deployed with sops to avoid a catch 22 # Skip ssh keys deployed with sops to avoid a catch 22
defaultImportKeys = algo: defaultImportKeys =
algo:
if config.services.openssh.enable then if config.services.openssh.enable then
map (e: e.path) (lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) config.services.openssh.hostKeys) map (e: e.path) (
lib.filter (
e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)
) config.services.openssh.hostKeys
)
else else
[]; [ ];
in { in
{
options.sops = { options.sops = {
secrets = lib.mkOption { secrets = lib.mkOption {
type = lib.types.attrsOf secretType; type = lib.types.attrsOf secretType;
default = {}; default = { };
description = '' description = ''
Path where the latest secrets are mounted to. Path where the latest secrets are mounted to.
''; '';
@ -208,14 +236,22 @@ in {
}; };
log = lib.mkOption { log = lib.mkOption {
type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); type = lib.types.listOf (
default = [ "keyImport" "secretChanges" ]; lib.types.enum [
"keyImport"
"secretChanges"
]
);
default = [
"keyImport"
"secretChanges"
];
description = "What to log"; description = "What to log";
}; };
environment = lib.mkOption { environment = lib.mkOption {
type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path);
default = {}; default = { };
description = '' description = ''
Environment variables to set before calling sops-install-secrets. Environment variables to set before calling sops-install-secrets.
@ -230,7 +266,7 @@ in {
package = lib.mkOption { package = lib.mkOption {
type = lib.types.package; type = lib.types.package;
default = (pkgs.callPackage ../.. {}).sops-install-secrets; default = (pkgs.callPackage ../.. { }).sops-install-secrets;
defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets"; defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets";
description = '' description = ''
sops-install-secrets package to use. sops-install-secrets package to use.
@ -240,9 +276,10 @@ in {
validationPackage = lib.mkOption { validationPackage = lib.mkOption {
type = lib.types.package; type = lib.types.package;
default = default =
if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then
then sops-install-secrets sops-install-secrets
else (pkgs.pkgsBuildHost.callPackage ../.. {}).sops-install-secrets; else
(pkgs.pkgsBuildHost.callPackage ../.. { }).sops-install-secrets;
defaultText = lib.literalExpression "config.sops.package"; defaultText = lib.literalExpression "config.sops.package";
description = '' description = ''
@ -326,40 +363,78 @@ in {
imports = [ imports = [
./templates ./templates
./secrets-for-users ./secrets-for-users
(lib.mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ]) (lib.mkRenamedOptionModule
(lib.mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ]) [
"sops"
"gnupgHome"
]
[
"sops"
"gnupg"
"home"
]
)
(lib.mkRenamedOptionModule
[
"sops"
"sshKeyPaths"
]
[
"sops"
"gnupg"
"sshKeyPaths"
]
)
]; ];
config = lib.mkMerge [ config = lib.mkMerge [
(lib.mkIf (cfg.secrets != {}) { (lib.mkIf (cfg.secrets != { }) {
assertions = [{ 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 =
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []); cfg.gnupg.home != null
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; || cfg.gnupg.sshKeyPaths != [ ]
}] ++ lib.optionals cfg.validateSopsFiles ( || cfg.age.keyFile != null
lib.concatLists (lib.mapAttrsToList (name: secret: [{ || cfg.age.sshKeyPaths != [ ];
assertion = builtins.pathExists secret.sopsFile; message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home";
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; }
} { {
assertion = assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ]);
builtins.isPath secret.sopsFile || message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
(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"; ]
} { ++ lib.optionals cfg.validateSopsFiles (
assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; lib.concatLists (
message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; lib.mapAttrsToList (name: secret: [
} { {
assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; assertion = builtins.pathExists secret.sopsFile;
message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
}]) cfg.secrets) }
); {
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";
}
{
assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null;
message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set";
}
{
assertion = secret.gid != null && secret.gid != 0 -> secret.group == null;
message = "In ${secret.name} exactly one of sops.group and sops.gid must be set";
}
]) cfg.secrets
)
);
sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) (
lib.mkDefault "${pkgs.gnupg}/bin/gpg"
);
# When using sysusers we no longer are started as an activation script because those are started in initrd while sysusers is started later. # When using sysusers we no longer are started as an activation script because those are started in initrd while sysusers is started later.
systemd.services.sops-install-secrets = lib.mkIf (regularSecrets != { } && useSystemdActivation) { systemd.services.sops-install-secrets = lib.mkIf (regularSecrets != { } && useSystemdActivation) {
wantedBy = [ "sysinit.target" ]; wantedBy = [ "sysinit.target" ];
after = [ "systemd-sysusers.service" ]; after = [ "systemd-sysusers.service" ];
environment = cfg.environment; environment = cfg.environment;
unitConfig.DefaultDependencies = "no"; unitConfig.DefaultDependencies = "no";
@ -372,27 +447,43 @@ in {
}; };
system.activationScripts = { system.activationScripts = {
setupSecrets = lib.mkIf (regularSecrets != {} && !useSystemdActivation) (lib.stringAfter ([ "specialfs" "users" "groups" ] ++ lib.optional cfg.age.generateKey "generate-age-key") '' setupSecrets = lib.mkIf (regularSecrets != { } && !useSystemdActivation) (
[ -e /run/current-system ] || echo setting up secrets... lib.stringAfter
${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"} (
'' // lib.optionalAttrs (config.system ? dryActivationScript) { [
supportsDryActivation = true; "specialfs"
}); "users"
"groups"
]
++ lib.optional cfg.age.generateKey "generate-age-key"
)
''
[ -e /run/current-system ] || echo setting up secrets...
${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"}
''
// lib.optionalAttrs (config.system ? dryActivationScript) {
supportsDryActivation = true;
}
);
generate-age-key = let generate-age-key =
escapedKeyFile = lib.escapeShellArg cfg.age.keyFile; let
in lib.mkIf cfg.age.generateKey (lib.stringAfter [] '' escapedKeyFile = lib.escapeShellArg cfg.age.keyFile;
if [[ ! -f ${escapedKeyFile} ]]; then in
echo generating machine-specific age key... lib.mkIf cfg.age.generateKey (
mkdir -p $(dirname ${escapedKeyFile}) lib.stringAfter [ ] ''
# age-keygen sets 0600 by default, no need to chmod. if [[ ! -f ${escapedKeyFile} ]]; then
${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} echo generating machine-specific age key...
fi mkdir -p $(dirname ${escapedKeyFile})
''); # age-keygen sets 0600 by default, no need to chmod.
${pkgs.age}/bin/age-keygen -o ${escapedKeyFile}
fi
''
);
}; };
}) })
{ {
system.build.sops-nix-manifest = manifest; system.build.sops-nix-manifest = manifest;
} }
]; ];
} }

View file

@ -4,26 +4,31 @@ suffix: secrets: templates: extraJson:
writeTextFile { writeTextFile {
name = "manifest${suffix}.json"; name = "manifest${suffix}.json";
text = builtins.toJSON ({ text = builtins.toJSON (
secrets = builtins.attrValues secrets; {
templates = builtins.attrValues templates; secrets = builtins.attrValues secrets;
# Does this need to be configurable? templates = builtins.attrValues templates;
secretsMountPoint = "/run/secrets.d"; # Does this need to be configurable?
symlinkPath = "/run/secrets"; secretsMountPoint = "/run/secrets.d";
keepGenerations = cfg.keepGenerations; symlinkPath = "/run/secrets";
gnupgHome = cfg.gnupg.home; keepGenerations = cfg.keepGenerations;
sshKeyPaths = cfg.gnupg.sshKeyPaths; gnupgHome = cfg.gnupg.home;
ageKeyFile = cfg.age.keyFile; sshKeyPaths = cfg.gnupg.sshKeyPaths;
ageSshKeyPaths = cfg.age.sshKeyPaths; ageKeyFile = cfg.age.keyFile;
useTmpfs = cfg.useTmpfs; ageSshKeyPaths = cfg.age.sshKeyPaths;
placeholderBySecretName = cfg.placeholder; useTmpfs = cfg.useTmpfs;
userMode = false; placeholderBySecretName = cfg.placeholder;
logging = { userMode = false;
keyImport = builtins.elem "keyImport" cfg.log; logging = {
secretChanges = builtins.elem "secretChanges" cfg.log; keyImport = builtins.elem "keyImport" cfg.log;
}; secretChanges = builtins.elem "secretChanges" cfg.log;
} // extraJson); };
}
// extraJson
);
checkPhase = '' checkPhase = ''
${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out" ${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${
if cfg.validateSopsFiles then "sopsfile" else "manifest"
} "$out"
''; '';
} }

View file

@ -1,8 +1,14 @@
{ lib, options, config, pkgs, ... }: {
lib,
options,
config,
pkgs,
...
}:
let let
cfg = config.sops; cfg = config.sops;
secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets;
templatesForUsers = {}; # We do not currently support `neededForUsers` for templates. templatesForUsers = { }; # We do not currently support `neededForUsers` for templates.
manifestFor = pkgs.callPackage ../manifest-for.nix { manifestFor = pkgs.callPackage ../manifest-for.nix {
inherit cfg; inherit cfg;
inherit (pkgs) writeTextFile; inherit (pkgs) writeTextFile;
@ -15,44 +21,54 @@ let
symlinkPath = "/run/secrets-for-users"; symlinkPath = "/run/secrets-for-users";
}; };
sysusersEnabled = options.systemd ? sysusers && config.systemd.sysusers.enable; sysusersEnabled = options.systemd ? sysusers && config.systemd.sysusers.enable;
useSystemdActivation = sysusersEnabled || useSystemdActivation =
(options.services ? userborn && config.services.userborn.enable); sysusersEnabled || (options.services ? userborn && config.services.userborn.enable);
in in
{ {
systemd.services.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != { } && useSystemdActivation) { systemd.services.sops-install-secrets-for-users =
wantedBy = [ "systemd-sysusers.service" ]; lib.mkIf (secretsForUsers != { } && useSystemdActivation)
before = [ "systemd-sysusers.service" ]; {
environment = cfg.environment; wantedBy = [ "systemd-sysusers.service" ];
unitConfig.DefaultDependencies = "no"; before = [ "systemd-sysusers.service" ];
environment = cfg.environment;
unitConfig.DefaultDependencies = "no";
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
ExecStart = [ "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}" ]; ExecStart = [ "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}" ];
RemainAfterExit = true; RemainAfterExit = true;
}; };
}; };
system.activationScripts = lib.mkIf (secretsForUsers != { } && !useSystemdActivation) { system.activationScripts = lib.mkIf (secretsForUsers != { } && !useSystemdActivation) {
setupSecretsForUsers = lib.stringAfter ([ "specialfs" ] ++ lib.optional cfg.age.generateKey "generate-age-key") '' setupSecretsForUsers =
[ -e /run/current-system ] || echo setting up secrets for users... lib.stringAfter ([ "specialfs" ] ++ lib.optional cfg.age.generateKey "generate-age-key") ''
${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"} [ -e /run/current-system ] || echo setting up secrets for users...
'' // lib.optionalAttrs (config.system ? dryActivationScript) { ${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"}
supportsDryActivation = true; ''
}; // lib.optionalAttrs (config.system ? dryActivationScript) {
supportsDryActivation = true;
};
users.deps = [ "setupSecretsForUsers" ]; users.deps = [ "setupSecretsForUsers" ];
}; };
assertions = [{ assertions = [
assertion = (lib.filterAttrs (_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")) secretsForUsers) == { }; {
message = "neededForUsers cannot be used for secrets that are not root-owned"; assertion =
} { (lib.filterAttrs (
assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers; _: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")
message = '' ) secretsForUsers) == { };
systemd.sysusers.enable in combination with sops.secrets.<name>.neededForUsers can only work with config.users.mutableUsers enabled. message = "neededForUsers cannot be used for secrets that are not root-owned";
See https://github.com/Mic92/sops-nix/issues/475 }
''; {
}]; assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers;
message = ''
systemd.sysusers.enable in combination with sops.secrets.<name>.neededForUsers can only work with config.users.mutableUsers enabled.
See https://github.com/Mic92/sops-nix/issues/475
'';
}
];
system.build.sops-nix-users-manifest = manifestForUsers; system.build.sops-nix-users-manifest = manifestForUsers;
} }

View file

@ -1,108 +1,123 @@
{ config, pkgs, lib, options, ... }: {
config,
pkgs,
lib,
options,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
mkDefault mkDefault
mapAttrs mapAttrs
types types
; ;
users = config.users.users; users = config.users.users;
in { in
{
options.sops = { options.sops = {
templates = mkOption { templates = mkOption {
description = "Templates for secret files"; description = "Templates for secret files";
type = types.attrsOf (types.submodule ({ config, ... }: { type = types.attrsOf (
options = { types.submodule (
name = mkOption { { config, ... }:
type = types.singleLineStr; {
default = config._module.args.name; options = {
description = '' name = mkOption {
Name of the file used in /run/secrets/rendered type = types.singleLineStr;
''; default = config._module.args.name;
}; description = ''
path = mkOption { Name of the file used in /run/secrets/rendered
description = "Path where the rendered file will be placed"; '';
type = types.singleLineStr; };
# Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` path = mkOption {
default = "/run/secrets/rendered/${config.name}"; description = "Path where the rendered file will be placed";
}; type = types.singleLineStr;
content = mkOption { # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go`
type = types.lines; default = "/run/secrets/rendered/${config.name}";
default = ""; };
description = '' content = mkOption {
Content of the file type = types.lines;
''; default = "";
}; description = ''
mode = mkOption { Content of the file
type = types.singleLineStr; '';
default = "0400"; };
description = '' mode = mkOption {
Permissions mode of the rendered secret file in octal. type = types.singleLineStr;
''; default = "0400";
}; description = ''
owner = mkOption { Permissions mode of the rendered secret file in octal.
type = types.singleLineStr; '';
default = "root"; };
description = '' owner = mkOption {
User of the file. type = types.singleLineStr;
''; default = "root";
}; description = ''
group = mkOption { User of the file.
type = types.singleLineStr; '';
default = users.${config.owner}.group; };
defaultText = lib.literalExpression ''config.users.users.''${cfg.owner}.group''; group = mkOption {
description = '' type = types.singleLineStr;
Group of the file. default = users.${config.owner}.group;
''; defaultText = lib.literalExpression ''config.users.users.''${cfg.owner}.group'';
}; description = ''
file = mkOption { Group of the file.
type = types.path; '';
default = pkgs.writeText config.name config.content; };
defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; file = mkOption {
example = "./configuration-template.conf"; type = types.path;
description = '' default = pkgs.writeText config.name config.content;
File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored. defaultText = lib.literalExpression ''pkgs.writeText config.name config.content'';
''; example = "./configuration-template.conf";
}; description = ''
restartUnits = lib.mkOption { File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored.
type = lib.types.listOf lib.types.str; '';
default = [ ]; };
example = [ "sshd.service" ]; restartUnits = lib.mkOption {
description = '' type = lib.types.listOf lib.types.str;
Names of units that should be restarted when the rendered template changes. default = [ ];
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />. example = [ "sshd.service" ];
''; description = ''
}; Names of units that should be restarted when the rendered template changes.
reloadUnits = lib.mkOption { This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
type = lib.types.listOf lib.types.str; '';
default = [ ]; };
example = [ "sshd.service" ]; reloadUnits = lib.mkOption {
description = '' type = lib.types.listOf lib.types.str;
Names of units that should be reloaded when the rendered template changes. default = [ ];
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />. example = [ "sshd.service" ];
''; description = ''
}; Names of units that should be reloaded when the rendered template changes.
}; This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
})); '';
};
};
}
)
);
default = { }; default = { };
}; };
placeholder = mkOption { placeholder = mkOption {
type = types.attrsOf (types.mkOptionType { type = types.attrsOf (
name = "coercibleToString"; types.mkOptionType {
description = "value that can be coerced to string"; name = "coercibleToString";
check = lib.strings.isConvertibleWithToString; description = "value that can be coerced to string";
merge = lib.mergeEqualOption; check = lib.strings.isConvertibleWithToString;
}); merge = lib.mergeEqualOption;
}
);
default = { }; default = { };
visible = false; visible = false;
}; };
}; };
config = lib.optionalAttrs (options ? sops.secrets) config = lib.optionalAttrs (options ? sops.secrets) (
(lib.mkIf (config.sops.templates != { }) { lib.mkIf (config.sops.templates != { }) {
sops.placeholder = mapAttrs sops.placeholder = mapAttrs (
(name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>") name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>"
config.sops.secrets; ) config.sops.secrets;
}); }
);
} }

View file

@ -2,11 +2,12 @@
sopsCall: sopsCall:
if cfg.environment == {} then if cfg.environment == { } then
sopsCall sopsCall
else '' else
( ''
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} (
${sopsCall} ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)}
) ${sopsCall}
'' )
''

View file

@ -1,13 +1,25 @@
{ makeSetupHook, gnupg, sops, lib }: {
makeSetupHook,
gnupg,
sops,
lib,
}:
let let
# FIXME: drop after 23.05 # FIXME: drop after 23.05
propagatedBuildInputs = if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then "deps" else "propagatedBuildInputs"; propagatedBuildInputs =
if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then
"deps"
else
"propagatedBuildInputs";
in in
(makeSetupHook { (makeSetupHook {
name = "sops-import-keys-hook"; name = "sops-import-keys-hook";
substitutions = { substitutions = {
gpg = "${gnupg}/bin/gpg"; gpg = "${gnupg}/bin/gpg";
}; };
${propagatedBuildInputs} = [ sops gnupg ]; ${propagatedBuildInputs} = [
sops
gnupg
];
} ./sops-import-keys-hook.bash) } ./sops-import-keys-hook.bash)

View file

@ -1,5 +1,5 @@
# shell.nix # shell.nix
with import <nixpkgs> {}; with import <nixpkgs> { };
mkShell { mkShell {
sopsPGPKeyDirs = [ sopsPGPKeyDirs = [
"./keys" "./keys"
@ -10,6 +10,6 @@ mkShell {
]; ];
sopsCreateGPGHome = "1"; sopsCreateGPGHome = "1";
nativeBuildInputs = [ nativeBuildInputs = [
(pkgs.callPackage ../../.. {}).sops-import-keys-hook (pkgs.callPackage ../../.. { }).sops-import-keys-hook
]; ];
} }

View file

@ -1,4 +1,12 @@
{ stdenv, lib, makeWrapper, gnupg, coreutils, util-linux, unixtools }: {
stdenv,
lib,
makeWrapper,
gnupg,
coreutils,
util-linux,
unixtools,
}:
stdenv.mkDerivation { stdenv.mkDerivation {
name = "sops-init-gpg-key"; name = "sops-init-gpg-key";
@ -11,9 +19,14 @@ stdenv.mkDerivation {
installPhase = '' installPhase = ''
install -m755 -D $src $out/bin/sops-init-gpg-key install -m755 -D $src $out/bin/sops-init-gpg-key
wrapProgram $out/bin/sops-init-gpg-key \ wrapProgram $out/bin/sops-init-gpg-key \
--prefix PATH : ${lib.makeBinPath [ --prefix PATH : ${
coreutils util-linux gnupg unixtools.hostname lib.makeBinPath [
]} coreutils
util-linux
gnupg
unixtools.hostname
]
}
''; '';
doInstallCheck = true; doInstallCheck = true;

View file

@ -1,9 +1,20 @@
{ lib, buildGoModule, stdenv, vendorHash, go, callPackages }: {
lib,
buildGoModule,
stdenv,
vendorHash,
go,
callPackages,
}:
buildGoModule { buildGoModule {
pname = "sops-install-secrets"; pname = "sops-install-secrets";
version = "0.0.1"; version = "0.0.1";
src = lib.sourceByRegex ../.. [ "go\.(mod|sum)" "pkgs" "pkgs/sops-install-secrets.*" ]; src = lib.sourceByRegex ../.. [
"go\.(mod|sum)"
"pkgs"
"pkgs/sops-install-secrets.*"
];
subPackages = [ "pkgs/sops-install-secrets" ]; subPackages = [ "pkgs/sops-install-secrets" ];
@ -12,19 +23,20 @@ buildGoModule {
passthru.tests = callPackages ./nixos-test.nix { }; passthru.tests = callPackages ./nixos-test.nix { };
outputs = [ "out" ] ++ outputs = [ "out" ] ++ lib.lists.optionals (stdenv.isLinux) [ "unittest" ];
lib.lists.optionals (stdenv.isLinux) [ "unittest" ];
postInstall = '' postInstall =
go test -c ./pkgs/sops-install-secrets ''
'' + lib.optionalString (stdenv.isLinux) '' go test -c ./pkgs/sops-install-secrets
# *.test is only tested on linux. $unittest does not exist on darwin. ''
install -D ./sops-install-secrets.test $unittest/bin/sops-install-secrets.test + lib.optionalString (stdenv.isLinux) ''
# newer versions of nixpkgs no longer require this step # *.test is only tested on linux. $unittest does not exist on darwin.
if command -v remove-references-to; then install -D ./sops-install-secrets.test $unittest/bin/sops-install-secrets.test
remove-references-to -t ${go} $unittest/bin/sops-install-secrets.test # newer versions of nixpkgs no longer require this step
fi if command -v remove-references-to; then
''; remove-references-to -t ${go} $unittest/bin/sops-install-secrets.test
fi
'';
inherit vendorHash; inherit vendorHash;

View file

@ -1,64 +1,75 @@
{ lib, testers }: { lib, testers }:
let let
userPasswordTest = name: extraConfig: testers.runNixOSTest { userPasswordTest =
inherit name; name: extraConfig:
nodes.machine = { config, lib, ... }: { testers.runNixOSTest {
imports = [ inherit name;
../../modules/sops nodes.machine =
extraConfig { config, lib, ... }:
]; {
sops = { imports = [
age.keyFile = "/run/age-keys.txt"; ../../modules/sops
defaultSopsFile = ./test-assets/secrets.yaml; extraConfig
secrets.test_key.neededForUsers = true; ];
secrets."nested/test/file".owner = "example-user"; sops = {
}; age.keyFile = "/run/age-keys.txt";
system.switch.enable = true; defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key.neededForUsers = true;
secrets."nested/test/file".owner = "example-user";
};
system.switch.enable = true;
users.users.example-user = lib.mkMerge [ users.users.example-user = lib.mkMerge [
(lib.mkIf (! config.systemd.sysusers.enable) { (lib.mkIf (!config.systemd.sysusers.enable) {
isNormalUser = true; isNormalUser = true;
hashedPasswordFile = config.sops.secrets.test_key.path; hashedPasswordFile = config.sops.secrets.test_key.path;
}) })
(lib.mkIf config.systemd.sysusers.enable { (lib.mkIf config.systemd.sysusers.enable {
isSystemUser = true; isSystemUser = true;
group = "users"; group = "users";
hashedPasswordFile = config.sops.secrets.test_key.path; hashedPasswordFile = config.sops.secrets.test_key.path;
}) })
]; ];
};
testScript =
''
start_all()
machine.wait_for_unit("multi-user.target")
machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work...
user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned...
assert user == "example-user", f"Expected 'example-user', got '{user}'"
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists
# BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test
''
+ lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) ''
machine.succeed("/run/current-system/bin/switch-to-configuration test")
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch
'';
}; };
in
testScript = '' {
start_all()
machine.wait_for_unit("multi-user.target")
machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work...
user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned...
assert user == "example-user", f"Expected 'example-user', got '{user}'"
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists
# BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test
'' + lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) ''
machine.succeed("/run/current-system/bin/switch-to-configuration test")
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch
'';
};
in {
ssh-keys = testers.runNixOSTest { ssh-keys = testers.runNixOSTest {
name = "sops-ssh-keys"; name = "sops-ssh-keys";
nodes.server = { ... }: { nodes.server =
imports = [ ../../modules/sops ]; { ... }:
services.openssh.enable = true; {
services.openssh.hostKeys = [{ imports = [ ../../modules/sops ];
type = "rsa"; services.openssh.enable = true;
bits = 4096; services.openssh.hostKeys = [
path = ./test-assets/ssh-key; {
}]; type = "rsa";
sops.defaultSopsFile = ./test-assets/secrets.yaml; bits = 4096;
sops.secrets.test_key = { }; path = ./test-assets/ssh-key;
}; }
];
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key = { };
};
testScript = '' testScript = ''
start_all() start_all()
@ -68,24 +79,26 @@ in {
pruning = testers.runNixOSTest { pruning = testers.runNixOSTest {
name = "sops-pruning"; name = "sops-pruning";
nodes.machine = { lib, ... }: { nodes.machine =
imports = [ ../../modules/sops ]; { lib, ... }:
sops = { {
age.keyFile = "/run/age-keys.txt"; imports = [ ../../modules/sops ];
defaultSopsFile = ./test-assets/secrets.yaml; sops = {
secrets.test_key = { }; age.keyFile = "/run/age-keys.txt";
keepGenerations = lib.mkDefault 0; defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
keepGenerations = lib.mkDefault 0;
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
specialisation.pruning.configuration.sops.keepGenerations = 10;
}; };
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
specialisation.pruning.configuration.sops.keepGenerations = 10;
};
testScript = '' testScript = ''
# Force us to generation 100 # Force us to generation 100
machine.succeed("mkdir /run/secrets.d/{2..99} /run/secrets.d/non-numeric") machine.succeed("mkdir /run/secrets.d/{2..99} /run/secrets.d/non-numeric")
@ -112,49 +125,51 @@ in {
age-keys = testers.runNixOSTest { age-keys = testers.runNixOSTest {
name = "sops-age-keys"; name = "sops-age-keys";
nodes.machine = { config, ... }: { nodes.machine =
imports = [ ../../modules/sops ]; { config, ... }:
sops = { {
age.keyFile = "/run/age-keys.txt"; imports = [ ../../modules/sops ];
defaultSopsFile = ./test-assets/secrets.yaml; sops = {
secrets = { age.keyFile = "/run/age-keys.txt";
test_key = { }; defaultSopsFile = ./test-assets/secrets.yaml;
secrets = {
test_key = { };
test_key_someuser_somegroup = { test_key_someuser_somegroup = {
uid = config.users.users."someuser".uid; uid = config.users.users."someuser".uid;
gid = config.users.groups."somegroup".gid; gid = config.users.groups."somegroup".gid;
key = "test_key"; key = "test_key";
}; };
test_key_someuser_root = { test_key_someuser_root = {
uid = config.users.users."someuser".uid; uid = config.users.users."someuser".uid;
key = "test_key"; key = "test_key";
}; };
test_key_root_root = { test_key_root_root = {
key = "test_key"; key = "test_key";
}; };
test_key_1001_1001 = { test_key_1001_1001 = {
uid = 1001; uid = 1001;
gid = 1001; gid = 1001;
key = "test_key"; key = "test_key";
};
}; };
}; };
};
users.users."someuser" = { users.users."someuser" = {
uid = 1000; uid = 1000;
group = "somegroup"; group = "somegroup";
isNormalUser = true; isNormalUser = true;
}; };
users.groups."somegroup" = { users.groups."somegroup" = {
gid = 1000; gid = 1000;
}; };
# must run before sops sets up keys # must run before sops sets up keys
boot.initrd.postDeviceCommands = '' boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt chmod -R 700 /run/age-keys.txt
''; '';
}; };
testScript = '' testScript = ''
start_all() start_all()
@ -183,10 +198,12 @@ in {
nodes.machine = { nodes.machine = {
imports = [ ../../modules/sops ]; imports = [ ../../modules/sops ];
services.openssh.enable = true; services.openssh.enable = true;
services.openssh.hostKeys = [{ services.openssh.hostKeys = [
type = "ed25519"; {
path = ./test-assets/ssh-ed25519-key; type = "ed25519";
}]; path = ./test-assets/ssh-ed25519-key;
}
];
sops = { sops = {
defaultSopsFile = ./test-assets/secrets.yaml; defaultSopsFile = ./test-assets/secrets.yaml;
@ -207,37 +224,39 @@ in {
pgp-keys = testers.runNixOSTest { pgp-keys = testers.runNixOSTest {
name = "sops-pgp-keys"; name = "sops-pgp-keys";
nodes.server = { lib, config, ... }: { nodes.server =
imports = [ ../../modules/sops ]; { lib, config, ... }:
{
imports = [ ../../modules/sops ];
users.users.someuser = { users.users.someuser = {
isSystemUser = true; isSystemUser = true;
group = "nogroup"; group = "nogroup";
};
sops.gnupg.home = "/run/gpghome";
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key.owner = config.users.users.someuser.name;
sops.secrets."nested/test/file".owner = config.users.users.someuser.name;
sops.secrets.existing-file = {
key = "test_key";
path = "/run/existing-file";
};
# must run before sops
system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] ''
cp -r ${./test-assets/gnupghome} /run/gpghome
chmod -R 700 /run/gpghome
touch /run/existing-file
'';
# Useful for debugging
#environment.systemPackages = [ pkgs.gnupg pkgs.sops ];
#environment.variables = {
# GNUPGHOME = "/run/gpghome";
# SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg";
# SOPSFILE = "${./test-assets/secrets.yaml}";
#};
}; };
sops.gnupg.home = "/run/gpghome";
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key.owner = config.users.users.someuser.name;
sops.secrets."nested/test/file".owner = config.users.users.someuser.name;
sops.secrets.existing-file = {
key = "test_key";
path = "/run/existing-file";
};
# must run before sops
system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] ''
cp -r ${./test-assets/gnupghome} /run/gpghome
chmod -R 700 /run/gpghome
touch /run/existing-file
'';
# Useful for debugging
#environment.systemPackages = [ pkgs.gnupg pkgs.sops ];
#environment.variables = {
# GNUPGHOME = "/run/gpghome";
# SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg";
# SOPSFILE = "${./test-assets/secrets.yaml}";
#};
};
testScript = '' testScript = ''
def assertEqual(exp: str, act: str) -> None: def assertEqual(exp: str, act: str) -> None:
if exp != act: if exp != act:
@ -260,47 +279,49 @@ in {
templates = testers.runNixOSTest { templates = testers.runNixOSTest {
name = "sops-templates"; name = "sops-templates";
nodes.machine = { config, ... }: { nodes.machine =
imports = [ ../../modules/sops ]; { config, ... }:
sops = { {
age.keyFile = "/run/age-keys.txt"; imports = [ ../../modules/sops ];
defaultSopsFile = ./test-assets/secrets.yaml; sops = {
secrets.test_key = { }; age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
# Verify that things work even with `neededForUsers` secrets. See # Verify that things work even with `neededForUsers` secrets. See
# <https://github.com/Mic92/sops-nix/issues/659>. # <https://github.com/Mic92/sops-nix/issues/659>.
secrets."nested/test/file".neededForUsers = true; secrets."nested/test/file".neededForUsers = true;
}; };
# must run before sops sets up keys # must run before sops sets up keys
boot.initrd.postDeviceCommands = '' boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt chmod -R 700 /run/age-keys.txt
'';
sops.templates.test_template = {
content = ''
This line is not modified.
The next value will be replaced by ${config.sops.placeholder.test_key}
This line is also not modified.
''; '';
mode = "0400";
owner = "someuser";
group = "somegroup";
};
sops.templates.test_default = {
content = ''
Test value: ${config.sops.placeholder.test_key}
'';
path = "/etc/externally/linked";
};
users.groups.somegroup = {}; sops.templates.test_template = {
users.users.someuser = { content = ''
isSystemUser = true; This line is not modified.
group = "somegroup"; The next value will be replaced by ${config.sops.placeholder.test_key}
This line is also not modified.
'';
mode = "0400";
owner = "someuser";
group = "somegroup";
};
sops.templates.test_default = {
content = ''
Test value: ${config.sops.placeholder.test_key}
'';
path = "/etc/externally/linked";
};
users.groups.somegroup = { };
users.users.someuser = {
isSystemUser = true;
group = "somegroup";
};
}; };
};
testScript = '' testScript = ''
def assertEqual(exp: str, act: str) -> None: def assertEqual(exp: str, act: str) -> None:
@ -337,62 +358,72 @@ in {
restart-and-reload = testers.runNixOSTest { restart-and-reload = testers.runNixOSTest {
name = "sops-restart-and-reload"; name = "sops-restart-and-reload";
nodes.machine = {config, ...}: { nodes.machine =
imports = [ ../../modules/sops ]; { config, ... }:
{
imports = [ ../../modules/sops ];
sops = { sops = {
age.keyFile = "/run/age-keys.txt"; age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml; defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { secrets.test_key = {
restartUnits = [ "restart-unit.service" "reload-unit.service" ]; restartUnits = [
reloadUnits = [ "reload-trigger.service" ]; "restart-unit.service"
"reload-unit.service"
];
reloadUnits = [ "reload-trigger.service" ];
};
templates.test_template = {
content = ''
this is a template with
a secret: ${config.sops.placeholder.test_key}
'';
restartUnits = [
"restart-unit.service"
"reload-unit.service"
];
reloadUnits = [ "reload-trigger.service" ];
};
};
system.switch.enable = true;
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
systemd.services."restart-unit" = {
description = "Restart unit";
# not started on boot
serviceConfig = {
ExecStart = "/bin/sh -c 'echo ok > /restarted'";
};
};
systemd.services."reload-unit" = {
description = "Reload unit";
wantedBy = [ "multi-user.target" ];
reloadIfChanged = true;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "/bin/sh -c true";
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
};
};
systemd.services."reload-trigger" = {
description = "Reload trigger unit";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "/bin/sh -c true";
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
};
}; };
templates.test_template = {
content = ''
this is a template with
a secret: ${config.sops.placeholder.test_key}
'';
restartUnits = [ "restart-unit.service" "reload-unit.service" ];
reloadUnits = [ "reload-trigger.service" ];
};
}; };
system.switch.enable = true;
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
systemd.services."restart-unit" = {
description = "Restart unit";
# not started on boot
serviceConfig = { ExecStart = "/bin/sh -c 'echo ok > /restarted'"; };
};
systemd.services."reload-unit" = {
description = "Reload unit";
wantedBy = [ "multi-user.target" ];
reloadIfChanged = true;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "/bin/sh -c true";
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
};
};
systemd.services."reload-trigger" = {
description = "Reload trigger unit";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "/bin/sh -c true";
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
};
};
};
testScript = '' testScript = ''
def assertOutput(output, *expected_lines): def assertOutput(output, *expected_lines):
expected_lines = list(expected_lines) expected_lines = list(expected_lines)
@ -524,32 +555,40 @@ in {
chmod -R 700 /run/age-keys.txt chmod -R 700 /run/age-keys.txt
''; '';
}; };
} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.05") { }
user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" ({ pkgs, ... }: { // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.05") {
systemd.sysusers.enable = true; user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" (
users.mutableUsers = true; { pkgs, ... }:
system.etc.overlay.enable = true; {
boot.initrd.systemd.enable = true; systemd.sysusers.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest; users.mutableUsers = true;
system.etc.overlay.enable = true;
# must run before sops sets up keys boot.initrd.systemd.enable = true;
systemd.services."sops-install-secrets-for-users".preStart = '' boot.kernelPackages = pkgs.linuxPackages_latest;
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
chmod -R 700 /run/age-keys.txt # must run before sops sets up keys
''; systemd.services."sops-install-secrets-for-users".preStart = ''
}); printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.11") { chmod -R 700 /run/age-keys.txt
user-passwords-userborn = userPasswordTest "sops-user-passwords-userborn" ({ pkgs, ... }: { '';
services.userborn.enable = true; }
users.mutableUsers = false; );
system.etc.overlay.enable = true; }
boot.initrd.systemd.enable = true; // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.11") {
boot.kernelPackages = pkgs.linuxPackages_latest; user-passwords-userborn = userPasswordTest "sops-user-passwords-userborn" (
{ pkgs, ... }:
# must run before sops sets up keys {
systemd.services."sops-install-secrets-for-users".preStart = '' services.userborn.enable = true;
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt users.mutableUsers = false;
chmod -R 700 /run/age-keys.txt system.etc.overlay.enable = true;
''; boot.initrd.systemd.enable = true;
}); boot.kernelPackages = pkgs.linuxPackages_latest;
# must run before sops sets up keys
systemd.services."sops-install-secrets-for-users".preStart = ''
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
}
);
} }

View file

@ -1,4 +1,11 @@
{ pkgs ? import <nixpkgs> {} }: {
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell { pkgs.mkShell {
nativeBuildInputs = with pkgs; [ go delve util-linux gnupg ]; nativeBuildInputs = with pkgs; [
go
delve
util-linux
gnupg
];
} }

View file

@ -1,13 +1,25 @@
{ makeSetupHook, gnupg, sops, lib }: {
makeSetupHook,
gnupg,
sops,
lib,
}:
let let
# FIXME: drop after 23.05 # FIXME: drop after 23.05
propagatedBuildInputs = if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then "deps" else "propagatedBuildInputs"; propagatedBuildInputs =
if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then
"deps"
else
"propagatedBuildInputs";
in in
(makeSetupHook { (makeSetupHook {
name = "sops-pgp-hook"; name = "sops-pgp-hook";
substitutions = { substitutions = {
gpg = "${gnupg}/bin/gpg"; gpg = "${gnupg}/bin/gpg";
}; };
${propagatedBuildInputs} = [ sops gnupg ]; ${propagatedBuildInputs} = [
sops
gnupg
];
} ./sops-pgp-hook.bash) } ./sops-pgp-hook.bash)

View file

@ -1,5 +1,5 @@
# shell.nix # shell.nix
with import <nixpkgs> {}; with import <nixpkgs> { };
mkShell { mkShell {
sopsPGPKeyDirs = [ sopsPGPKeyDirs = [
"./keys" "./keys"
@ -9,6 +9,6 @@ mkShell {
"./non-existing-key.gpg" "./non-existing-key.gpg"
]; ];
nativeBuildInputs = [ nativeBuildInputs = [
(pkgs.callPackage ../../.. {}).sops-pgp-hook (pkgs.callPackage ../../.. { }).sops-pgp-hook
]; ];
} }

View file

@ -1,16 +1,21 @@
{ pkgs ? import <nixpkgs> {} {
pkgs ? import <nixpkgs> { },
}: }:
let let
sopsPkgs = import ../. { inherit pkgs; }; sopsPkgs = import ../. { inherit pkgs; };
in pkgs.stdenv.mkDerivation { in
pkgs.stdenv.mkDerivation {
name = "env"; name = "env";
nativeBuildInputs = with pkgs; [ nativeBuildInputs =
bashInteractive with pkgs;
gnupg [
util-linux bashInteractive
nix gnupg
sopsPkgs.sops-pgp-hook-test util-linux
] ++ pkgs.lib.optional (pkgs.stdenv.isLinux) sopsPkgs.sops-install-secrets.unittest; nix
sopsPkgs.sops-pgp-hook-test
]
++ pkgs.lib.optional (pkgs.stdenv.isLinux) sopsPkgs.sops-install-secrets.unittest;
# allow to prefetch shell dependencies in build phase # allow to prefetch shell dependencies in build phase
dontUnpack = true; dontUnpack = true;
installPhase = '' installPhase = ''

View file

@ -1,4 +1,6 @@
{ pkgs ? import <nixpkgs> {} }: {
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell { pkgs.mkShell {
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
bashInteractive bashInteractive