This commit is contained in:
Ryota 2026-02-10 12:11:54 +00:00 committed by GitHub
commit ed14334b6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 262 additions and 3 deletions

View file

@ -274,6 +274,34 @@ in
Paths to ssh keys added as age keys during sops description.
'';
};
# Options for hardware key support (YubiKey, FIDO2, etc.)
systemdDeps = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "pcscd.socket" ];
description = ''
Additional systemd units that the sops-nix user service should depend on.
This is useful when using age plugins that require external services like pcscd.
'';
};
requirePcscd = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether pcscd (PC/SC Smart Card Daemon) is required for age decryption.
Enable this when using hardware key plugins like age-plugin-yubikey
or age-plugin-fido2-hmac.
This adds a pre-start check to wait for pcscd to be available before
attempting decryption.
Note: You must also enable `services.pcscd.enable = true` in your
NixOS configuration. The pcscd service runs at the system level and
will be socket-activated when the YubiKey is accessed.
'';
};
};
gnupg = {
@ -377,9 +405,15 @@ in
);
};
# Note: pcscd.socket is a system service, not a user service, so we cannot
# add it as a direct dependency for requirePcscd. Instead, we add a pre-start
# script that waits for pcscd to be available.
systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
Unit = {
Description = "sops-nix activation";
After = cfg.age.systemdDeps
++ lib.optional cfg.age.requirePcscd "yubikey-touch-detector.service";
Wants = cfg.age.systemdDeps;
};
Service = {
Type = "oneshot";
@ -387,9 +421,35 @@ in
lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment
);
ExecStart = script;
ExecStartPre = lib.mkIf cfg.age.requirePcscd [
"${pkgs.writeShellScript "wait-for-pcscd" ''
# Ensure pcscd is available for YubiKey communication.
# When pcscd.socket is enabled, systemd creates /run/pcscd/pcscd.comm
# and starts pcscd.service on-demand when the socket is accessed.
i=0
while [ $i -lt 30 ]; do
# Check if the pcscd socket file exists - this is the most reliable check
# and doesn't require D-Bus access
if [ -e /run/pcscd/pcscd.comm ]; then
exit 0
fi
sleep 0.2
i=$((i + 1))
done
echo "Warning: pcscd socket not found at /run/pcscd/pcscd.comm" >&2
echo "YubiKey decryption may fail. Ensure services.pcscd.enable = true" >&2
''}"
];
};
Install.WantedBy =
if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ];
# When pcscd is required, we need to wait for the graphical session to be active
# so that polkit recognizes it as an active session and allows pcscd access.
# Otherwise, we run at default.target for faster boot times.
if cfg.gnupg.home != null || cfg.age.requirePcscd
then [ "graphical-session-pre.target" ]
else [ "default.target" ];
};
# Darwin: load secrets once on login

View file

@ -316,6 +316,22 @@ in
List of plugins to use for sops decryption.
'';
};
# Options for hardware key support (YubiKey, FIDO2, etc.)
requirePcscd = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether pcscd (PC/SC Smart Card Daemon) is required for age decryption.
Enable this when using hardware key plugins like age-plugin-yubikey
or age-plugin-fido2-hmac.
On macOS, the system's built-in smart card services (CryptoTokenKit)
typically handle YubiKey communication automatically. This option
is provided for consistency with Linux but may not require additional
configuration on macOS.
'';
};
};
gnupg = {

View file

@ -369,6 +369,43 @@ in
Paths to ssh keys added as age keys during sops description.
'';
};
# Options for hardware key support (YubiKey, FIDO2, etc.)
activationScriptDeps = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "setupPcscdForSops" ];
description = ''
Additional activation script names that must complete before
setupSecrets and setupSecretsForUsers run. This is useful when
using age plugins that require external services like pcscd.
'';
};
systemdDeps = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "pcscd.socket" ];
description = ''
Additional systemd units that sops-install-secrets should depend on
when using systemd activation mode. This is useful when using age
plugins that require external services like pcscd.
'';
};
requirePcscd = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether pcscd (PC/SC Smart Card Daemon) is required for age decryption.
Enable this when using hardware key plugins like age-plugin-yubikey
or age-plugin-fido2-hmac. This automatically configures the necessary
dependencies to ensure pcscd is running before secrets are decrypted.
Note: You must also enable `services.pcscd.enable = true` in your
NixOS configuration for this to work.
'';
};
};
gnupg = {
@ -467,7 +504,8 @@ in
# 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 != { } && cfg.useSystemdActivation) {
wantedBy = [ "sysinit.target" ];
after = [ "systemd-sysusers.service" "userborn.service" ];
after = [ "systemd-sysusers.service" "userborn.service" ] ++ cfg.age.systemdDeps;
wants = cfg.age.systemdDeps;
requiredBy = [ "sysinit-reactivation.target" ];
before = [ "sysinit-reactivation.target" ];
environment = cfg.environment;
@ -491,6 +529,7 @@ in
"groups"
]
++ lib.optional cfg.age.generateKey "generate-age-key"
++ cfg.age.activationScriptDeps
)
''
[ -e /run/current-system ] || echo setting up secrets...
@ -520,5 +559,40 @@ in
{
system.build.sops-nix-manifest = manifest;
}
# Automatic pcscd configuration for hardware key plugins
(lib.mkIf cfg.age.requirePcscd {
assertions = [
{
assertion = config.services.pcscd.enable or false;
message = ''
sops.age.requirePcscd is enabled but services.pcscd.enable is not set.
Please add `services.pcscd.enable = true;` to your configuration.
'';
}
];
# Add pcscd.socket as a systemd dependency
sops.age.systemdDeps = [ "pcscd.socket" ];
# For activation script mode, ensure pcscd is started before secrets
system.activationScripts.setupPcscdForSops = lib.mkIf (!cfg.useSystemdActivation) (
lib.stringAfter [ "specialfs" ] ''
# Ensure pcscd drivers are available
mkdir -p /var/lib/pcsc
ln -sfn ${pkgs.ccid}/pcsc/drivers /var/lib/pcsc/drivers
# Try to start pcscd via socket activation, or directly if needed
if ! ${pkgs.systemd}/bin/systemctl is-active --quiet pcscd.socket 2>/dev/null; then
if ! ${pkgs.systemd}/bin/systemctl is-active --quiet pcscd.service 2>/dev/null; then
# Start pcscd directly with auto-exit for activation script context
${pkgs.pcsclite}/bin/pcscd --auto-exit 2>/dev/null || true
fi
fi
''
);
sops.age.activationScriptDeps = lib.mkIf (!cfg.useSystemdActivation) [ "setupPcscdForSops" ];
})
];
}

View file

@ -35,6 +35,8 @@ in
{
wantedBy = [ "systemd-sysusers.service" ];
before = [ "systemd-sysusers.service" ];
after = cfg.age.systemdDeps;
wants = cfg.age.systemdDeps;
environment = cfg.environment;
unitConfig.DefaultDependencies = "no";
path = cfg.age.plugins;
@ -48,7 +50,11 @@ in
system.activationScripts = lib.mkIf (secretsForUsers != { } && !useSystemdActivation) {
setupSecretsForUsers =
lib.stringAfter ([ "specialfs" ] ++ lib.optional cfg.age.generateKey "generate-age-key") ''
lib.stringAfter (
[ "specialfs" ]
++ lib.optional cfg.age.generateKey "generate-age-key"
++ cfg.age.activationScriptDeps
) ''
[ -e /run/current-system ] || echo setting up secrets for users...
${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"}
''