Agenix age-plugin-yubikey doesn't detect yubikey during boot

Hey, I set up agenix along with age-plugin-yubikey but during boot the yubikey isn’t detected. This is my config: GitHub - codebam/nixos (#nixos-desktop) It’s commented out right now because it doesn’t work, but it’s all there. It applies and works with nixos-rebuild switch after booting.

secrets.nix

let
  users = {
    yubikey-5c = "age1yubikey1q24yc9023p70svqhqhpftn5cqfd25f2wnpapazf470cs0cl0a9p6cq44w7m";
    yubikey-5c-nfc = "age1yubikey1qww4auye54gu430kzf37jww93aqt2gr4qx7d4xm2ukturxlr5uglgypnr2s";
  };
in
{
  "hashedpassword.age".publicKeys = [
    users.yubikey-5c
    users.yubikey-5c-nfc
  ];
  "github_token.age".publicKeys = [
    users.yubikey-5c
    users.yubikey-5c-nfc
  ];
}

configuration.nix

  age = {
    identityPaths = [ ./secrets/identities/yubikey-5c.txt ./secrets/identities/yubikey-5c-nfc.txt ];
    secrets.hashedpassword.file = ./secrets/hashedpassword.age;
    ageBin = "PATH=$PATH:${lib.makeBinPath [pkgs.age-plugin-yubikey]} ${pkgs.rage}/bin/rage";
  };

home.nix

  age = {
    identityPaths = [ ./secrets/identities/yubikey-5c.txt ./secrets/identities/yubikey-5c-nfc.txt ];
    secrets.github_token.file = ./secrets/github_token.age;
  };

I am wildly guessing here, since I have not used any age plugins, however looking at the way agenix gets actually called on boot/at runtime this all seems very much tied to the NixOS “activation scripts” meaning it lives outside the systemd configuration (unless I’m mistaken), and it seems to start right after the specialfs stuff.
This makes sense given that one might want to pass age secrets to systemd itself.
However it also means that when your secrets are decrypted a lot of hardware initialisation is not finished yet (something something udev something).

At least that’s my guess here.
As for a solution; I don’t really have a general one.
What I would do personally is probably to convince udev to initialize the device while still running the initrd (which is also systemd for me, meaning udev kind of already exists there), making it so the yubikey is already fully active by the time NixOS activation happens, but that’s a very specific and very hacky solution (it would work, if I guessed the problem correctly, but “move everything earlier in the boot process” only works so often until everything is earlier in the boot process).

In the context of the activation scripts, something which I cannot emphasise enough I have never worked with before (I only know about them because I know how systems usually boot), however you could try adding an additional dependency on the “udevd” activation script and hope it works.
Again, I don’t have that setup and all I’m doing is guessing, but maybe this could work for you:

{ lib, ... }:
{
  config.system.activationScripts.agenixNewGeneration = lib.mkAfter [ "udevd" ];
}

I couldn’t set config values due to:

error: Module `/nix/store/ncdn8i78lyi7s7ry7wnf3mqvmx1cl5a6-source/configuration.nix' has an unsupported attribute `boot'. This is caused by introducing a top-level `config' or `options' attribute. Add configuration attributes immediately on the top level instead, or move all of them (namely: boot environment fonts hardware networking nix nixpkgs programs security services stylix system systemd time users virtualisation xdg zramSwap) into the explicit `config' attribute.

If I set it using system.activationScripts.agenixNewGeneration = lib.mkAfter [ "udevd" ]; I get this error:

error: A definition for option `system.activationScripts.agenixNewGeneration' is not of type `string or (submodule)'. Definition values:
       - In `/nix/store/655ff8jxjk89krppbcdy5wrfr9zs6cdi-source/configuration.nix':
           [
             "udevd"
           ]

From looking at this, it seems like it would need to override the original activation script but I could be wrong.

I managed to change it, but applying this didn’t work.

Ah, yeah, that error is because you can write modules in two ways:

# config is toplevel
{
  my.option = true;
}

# config is an attr
{
  # this is how you can set `options` for instance
  #options = {};

  config = {
    my.option = true;
  };
}

And me using the latter one breaks on your end of course.
Just removing the config is enough, which I assume is what you did.

Dang it, copy&paste got me.
It should’ve been system.activationScripts.agenixNewGeneration.deps = ....
My mistake.
I just tested and this should be enough to set the option:

# I am literally copy&pasting this from the shell in which I got it to work
{ lib, ... }: { config.system.activationScripts.agenixNewGeneration.deps = lib.mkAfter [ "udevd" ]; }

If it really does not work (i.e. if the change was in fact applied correctly) you should be seeing that when diffing the two systems’ activate script the udevd section has moved up a bit:

example change
git diff --no-index /run/current-system/activate /nix/store/m5z6np7msvzgkwwi1xvx90y86bzs490p-nixos-system-gnutoo-24.11/activate
diff --git a/run/current-system/activate b/nix/store/m5z6np7msvzgkwwi1xvx90y86bzs490p-nixos-system-gnutoo-24.11/activate
index 075f681..410d7e4 100755
--- a/run/current-system/activate
+++ b/nix/store/m5z6np7msvzgkwwi1xvx90y86bzs490p-nixos-system-gnutoo-24.11/activate
@@ -2,7 +2,7 @@
 
 source /nix/store/mhxn5kwnri3z9hdzi3x0980id65p0icn-lib.sh
 
-systemConfig='/nix/store/qgiy8ga8fl99mfvwqg7c5y8qzj7gd3lh-nixos-system-gnutoo-24.11'
+systemConfig='/nix/store/m5z6np7msvzgkwwi1xvx90y86bzs490p-nixos-system-gnutoo-24.11'
 
 export PATH=/empty
 for i in /nix/store/440q5scq8paszj2sdgz98hxl1rz12i88-coreutils-9.5 /nix/store/0mqngkz34kv5z6hz91bbbgzgrnz56c4y-gnugrep-3.11 /nix/store/lpz4m485bm2y8mann5c4xcf4p4hlls3k-findutils-4.10.0 /nix/store/4b6d8hbgvln086jf1h7ra5vhj2v2gc9y-getent-glibc-2.40-66 /nix/store/387k8pgbh1b3skack03myc2phxnc7b5z-glibc-2.40-66-bin /nix/store/g5hr0hcd89f8n93v6b7ypd74hlm7h92q-shadow-4.16.0 /nix/store/7ydwl50wax5z380ayxf9lp5mrn4ldm84-net-tools-2.10 /nix/store/q6lh1ywsmby6sp96bnh8f1jlnwxvfkva-util-linux-2.39.4-bin; do
@@ -38,6 +38,23 @@ if (( _localstatus > 0 )); then
   printf "Activation script snippet '%s' failed (%s)\n" "specialfs" "$_localstatus"
 fi
 
+#### Activation script snippet udevd:
+_localstatus=0
+# The deprecated hotplug uevent helper is not used anymore
+if [ -e /proc/sys/kernel/hotplug ]; then
+  echo "" > /proc/sys/kernel/hotplug
+fi
+
+# Allow the kernel to find our firmware.
+if [ -e /sys/module/firmware_class/parameters/path ]; then
+  echo -n "/nix/store/32fqxaydbn71zdr40my6gbkjlqi0rb1f-firmware/lib/firmware" > /sys/module/firmware_class/parameters/path
+fi
+
+
+if (( _localstatus > 0 )); then
+  printf "Activation script snippet '%s' failed (%s)\n" "udevd" "$_localstatus"
+fi
+
 #### Activation script snippet agenixNewGeneration:
 _localstatus=0
 _agenix_generation="$(basename "$(readlink /run/agenix)" || echo 0)"
@@ -178,7 +195,7 @@ fi
 _localstatus=0
 # Set up the statically computed bits of /etc.
 echo "setting up /etc..."
-/nix/store/asij0gcqkgz0xmr109mqm493sppbxg0r-perl-5.40.0-env/bin/perl /nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl /nix/store/d9zzszy8782ki3p50czmavcz6ar93qyv-etc/etc
+/nix/store/asij0gcqkgz0xmr109mqm493sppbxg0r-perl-5.40.0-env/bin/perl /nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl /nix/store/l1p95h58y5c7g05d5gp7fhmb2jb6wvmi-etc/etc
 
 
 if (( _localstatus > 0 )); then
@@ -249,23 +266,6 @@ if (( _localstatus > 0 )); then
   printf "Activation script snippet '%s' failed (%s)\n" "no-nix-channel" "$_localstatus"
 fi
 
-#### Activation script snippet udevd:
-_localstatus=0
-# The deprecated hotplug uevent helper is not used anymore
-if [ -e /proc/sys/kernel/hotplug ]; then
-  echo "" > /proc/sys/kernel/hotplug
-fi
-
-# Allow the kernel to find our firmware.
-if [ -e /sys/module/firmware_class/parameters/path ]; then
-  echo -n "/nix/store/32fqxaydbn71zdr40my6gbkjlqi0rb1f-firmware/lib/firmware" > /sys/module/firmware_class/parameters/path
-fi
-
-
-if (( _localstatus > 0 )); then
-  printf "Activation script snippet '%s' failed (%s)\n" "udevd" "$_localstatus"
-fi
-
 #### Activation script snippet usrbinenv:
 _localstatus=0
 mkdir -p /usr/bin

If that happened but it still doesn’t work, then either my guess about what’s wrong was itself wrong, or the udevd activation script does not actually initialise the things it’d need to initialise for this to work (and looking at the diff above, yeah, it kinda doesn’t).
In that case I’m a bit lost, because I’m relatively sure that udev is not fully active when the activation script runs (though I can’t confirm that right now).

At the very least I can point at the NixOS LUKS code which has to deal with pretty much the same problem somehow, so maybe taking a bit of that code, writing your own activationScript and then making the agenix one depend on yours could work.
Maybe there are additional upstream docs for this (I have to admit I haven’t really checked)?

However I wouldn’t know since my systems use systemd in initrd for the LUKS crypto so even there udev is already present and hooked into everything.

Either way, since I’m out of specific actionable ideas on what to do I’ll wish you good luck with that issue, sorry I couldn’t help more.

Yeah didn’t work. This is the diff. I’ll look through the LUKS code you linked and try to implement that. Thanks for the suggestions anyways :slight_smile:

❯ git diff --no-index /run/current-system/activate /nix/store/y2vg3n6ci42skh8f7zblvndyk9lszz9p-nixos-system-nixos-desktop-25.11.20250528.7b3486b/activate
diff --git a/run/current-system/activate b/nix/store/y2vg3n6ci42skh8f7zblvndyk9lszz9p-nixos-system-nixos-desktop-25.11.20250528.7b3486b/activate
index 967f52c..cd03b05 100755
--- a/run/current-system/activate
+++ b/nix/store/y2vg3n6ci42skh8f7zblvndyk9lszz9p-nixos-system-nixos-desktop-25.11.20250528.7b3486b/activate
@@ -2,7 +2,7 @@

 source /nix/store/mhxn5kwnri3z9hdzi3x0980id65p0icn-lib.sh

-systemConfig='/nix/store/bw677a9ljzi9y5fhcbgb27yxh83cnmpx-nixos-system-nixos-desktop-25.11.20250528.7b3486b'
+systemConfig='/nix/store/y2vg3n6ci42skh8f7zblvndyk9lszz9p-nixos-system-nixos-desktop-25.11.20250528.7b3486b'

 export PATH=/empty
 for i in /nix/store/87fck6hm17chxjq7badb11mq036zbyv9-coreutils-9.7 /nix/store/gqmr3gixlddz3667ba1iyqck3c0dkpvd-gnugrep-3.11 /nix/store/7y59hzi3svdj1xjddjn2k7km96pifcyl-findutils-4.10.0 /nix/store/j2v7jjnczkj7ra7jsgq6kv3242a1l52x-getent-glibc-2.40-66 /nix/store/303islqk386z1w2g1ngvxnkl4glfpgrs-glibc-2.40-66-bin /nix/store/b895xnbwyfj1msj6ljcsvwfdhwqhd2vd-shadow-4.17.4 /nix/store/skd9hg5cdz7jwpq1wp38fvzab9y8p0m6-net-tools-2.10 /nix/store/af291yai47szhz3miviwslzrjqky31xw-util-linux-2.41-bin; do
@@ -15,6 +15,35 @@ trap "_status=1 _localstatus=\$?" ERR
 # Ensure a consistent umask.
 umask 0022

+#### Activation script snippet udevd:
+_localstatus=0
+# The deprecated hotplug uevent helper is not used anymore
+if [ -e /proc/sys/kernel/hotplug ]; then
+  echo "" > /proc/sys/kernel/hotplug
+fi
+
+# Allow the kernel to find our firmware.
+if [ -e /sys/module/firmware_class/parameters/path ]; then
+  echo -n "/nix/store/1i4iz3z0b4f4qmbd9shs5slgfihs88vc-firmware/lib/firmware" > /sys/module/firmware_class/parameters/path
+fi
+
+
+if (( _localstatus > 0 )); then
+  printf "Activation script snippet '%s' failed (%s)\n" "udevd" "$_localstatus"
+fi
+
+#### Activation script snippet agenixNewGeneration:
+_localstatus=0
+if ! command -v age >/dev/null 2>&1; then
+  echo "Error: age command not found, agenix may not work correctly."
+  exit 1
+fi
+
+
+if (( _localstatus > 0 )); then
+  printf "Activation script snippet '%s' failed (%s)\n" "agenixNewGeneration" "$_localstatus"
+fi
+
 #### Activation script snippet stdio:
 _localstatus=0

@@ -143,23 +172,6 @@ if (( _localstatus > 0 )); then
   printf "Activation script snippet '%s' failed (%s)\n" "persist-files" "$_localstatus"
 fi

-#### Activation script snippet udevd:
-_localstatus=0
-# The deprecated hotplug uevent helper is not used anymore
-if [ -e /proc/sys/kernel/hotplug ]; then
-  echo "" > /proc/sys/kernel/hotplug
-fi
-
-# Allow the kernel to find our firmware.
-if [ -e /sys/module/firmware_class/parameters/path ]; then
-  echo -n "/nix/store/1i4iz3z0b4f4qmbd9shs5slgfihs88vc-firmware/lib/firmware" > /sys/module/firmware_class/parameters/path
-fi
-
-
-if (( _localstatus > 0 )); then
-  printf "Activation script snippet '%s' failed (%s)\n" "udevd" "$_localstatus"
-fi
-
 #### Activation script snippet usrbinenv:
 _localstatus=0
 mkdir -p /usr/bin
1 Like