I log in to my laptop with a YubiKey. PIN, touch, desktop. No password typed.
Then the first app that wants a stored secret starts and GNOME throws up a box: “An application wants access to the keyring ‘Login’, but it is locked.” Every login. Same box, same question, a password I had just been told I did not need.
No single piece is broken. Every part is doing what it was built to do. The trouble is that the keyring was designed on the assumption that a login always involves a password, and a passwordless login breaks that assumption without telling anybody.
Why A Key Login Starves The Keyring
The login keyring is where the desktop keeps the secrets apps hand it: Wi-Fi keys, browser storage keys, the tokens chat clients, password managers and conferencing apps use to stay signed in. It sits on disk encrypted with your login password, and the Secret Service API1 is how apps ask for what is inside.
Normally you never see it. You type your password at the login screen, the PAM stack checks it against the account, and a keyring module further down the same stack quietly picks up that same password and uses it to open the keyring before your desktop has even finished drawing. One password, two jobs.
My stack starts with this line:
auth sufficient pam_u2f.so cue
sufficient means a successful YubiKey ends authentication there and then. That is exactly what Yubico document for passwordless login2. It works. It also means the password step never runs, so there is no password in the stack for the keyring module to pick up. The keyring stays locked until something asks for it, and the something is whichever app starts first.
The Usual Fixes, And What Each One Costs
There are four common answers to this. Every one of them gives something up.
| Fix | What you get | What it costs |
|---|---|---|
| Type the password at the prompt | Nothing to build | The prompt, every login. The key saved you nowt |
| Blank keyring password | Prompt gone | Every stored secret sits on disk unencrypted |
| Password and key at login | Proper two-factor, keyring opens itself | Typing the password every login, which is what the key was for |
| TPM-sealed password, unlocked at login | No prompt, no typing | Anyone who gets a session on that machine gets the secrets. No PIN, no key involved |
| YubiKey hmac-secret (this post) | No prompt, PIN + touch, password login still works | A second PIN and touch after login, and a script to look after |
The blank password is the one most forum threads end on, and it is the worst of the lot. The TPM route is respectable, but the TPM never asks you anything, so what it protects is a disk pulled out of the laptop, not a session somebody has sat down at while you made a brew. I wanted the keyring tied to the same thing my login is tied to: the key, and the PIN that goes with it.
The Key Can Hold A Secret
A FIDO2 key does more than say yes or no. The CTAP 2.1 standard has an extension called hmac-secret, which “is used by the platform to retrieve a symmetric secret from the authenticator when it needs to encrypt or decrypt data using that symmetric secret”3.
The way it works is simple enough. When you create a credential, the key generates random values and keeps them inside itself. Later you send it a salt, and it hands back an HMAC of that salt keyed with its hidden value. Same credential, same salt, same answer every time. Different key, nothing.
The detail that matters here: the key holds two of those hidden values, CredRandomWithUV and CredRandomWithoutUV3. UV is user verification, which on this key means the PIN. Ask with the PIN and you get one secret. Ask with only a touch and you get a completely different one. So the PIN is not a check bolted on in front of the secret. It is part of the secret.
| Input | Where it lives | On its own it gets you |
|---|---|---|
| Credential ID and salt | ~/.local/share/yubikey-keyring/, plain files | Nothing. They are not secret |
| Encrypted keyring password | Same folder, password.enc | Nothing without the decryption secret |
| Hidden credential value | Inside the YubiKey, never leaves it | Nothing without the salt and a touch |
| PIN | Your head | Selects the with-PIN secret. Wrong PIN, no secret, and the key counts the attempt |
All four together give the account password back. Take any one away and you are at the prompt, which is where you would have been anyway.
What I Built With Claude
The keyring password is my account password, on purpose. That keeps the password login working the normal way: type it at the login screen and the PAM stack opens the keyring itself, no script involved. The YubiKey path is the one that needs help.
| Piece | Job |
|---|---|
yubikey-keyring-setup | One-off. Creates the FIDO2 credential on the key with hmac-secret enabled. Needs the PIN and a touch |
yubikey-keyring-reset | Asks for the account password, checks it against the real account with unix_chkpwd before touching anything, encrypts it with the PIN-gated secret, backs up the old keyring and creates the new one |
yubikey-keyring-unlock | Runs at login. PIN dialog, touch, decrypt, unlock. Two goes at the PIN, then it gives up and leaves you the normal prompt |
wait-for-keyring | Wraps the autostart entries of every app that reads the keyring. Holds each one for up to 60 seconds until the keyring is open |
The secret comes out of fido2-assert4 with -h for hmac-secret, -p for touch and -v for the PIN. The password is encrypted with OpenSSL, AES-256 with 200,000 rounds of PBKDF2 on the derived secret. At login the password only ever travels down a pipe. The derived secret and, during a reset, the typed password sit briefly in a private temporary directory under /tmp, which on Fedora is RAM-backed tmpfs, and are deleted as soon as they are used. Neither ever goes on a command line where ps could see it.
wait-for-keyring is the piece people would skip. Don’t. Autostart order is not guaranteed, and if one app asks for its token a second before I have finished typing the PIN, GNOME raises the old prompt on top of mine, and the fix looks broken when it is not.
The scripts are yours to take: yubikey-keyring.zip, SHA-256 9b3670cf0f38c1b93e7738d2f68abef0380786d918491973e3ebefa5143e6263. It holds the scripts, an installer, a README and the test harness described below. No credential, salt or password is in it. Those are made on your own machine when you run the setup. Read the README, then run test/run.sh before you let any of it near your real keyring.
What The Build Turned Up
This was meant to be an hour’s work. Most of the evening went on things the documentation does not mention.
The daemon had changed underneath me
Every guide I have read for this problem talks to gnome-keyring-daemon, and every one of them would have sent me in the wrong direction, because the process answering on my machine was oo7-daemon 0.7.0, a beta. The laptop had gone to Fedora 45 earlier the same evening, a 5,464-step dnf transaction, and oo7 had arrived with it.
oo7 is a Rust implementation of the Secret Service, with its own server and its own PAM module5. To its credit, it implements gnome-keyring’s private D-Bus interface for unlocking with a password, so existing tools carry on working. That interface is called org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface6. The name tells you how far to trust it. It works, I have tested it, and I would still expect it to move one day.
The first password you type becomes the keyring password
This one explained the whole evening.
When oo7 starts and finds no keyring file it does not create one, it shows apps a locked placeholder called login, and the first password anybody uses to unlock that placeholder becomes the keyring password, though the file itself is only written when an app stores its first secret.
My keyring file was created at 20:45, and its first item came from an app that had started a few seconds earlier. So at 20:45 I typed something into a prompt I wanted out of the way, and that became the password. Nothing I tried afterwards opened it. The unlock code was not at fault either: I proved that on a throwaway keyring with a known password, which locked, unlocked and deleted cleanly.
I then checked the placeholder on a throwaway daemon. Before the file exists, any password unlocks it, including a wrong one. So nothing tells you at the time that you have just set a password. You find out at the next login, when the right password, as far as you know it, is refused.
| State | Unlock with any password | What gets written |
|---|---|---|
| No keyring file, placeholder shown | Succeeds | Nothing yet |
| First item stored | n/a | File created, encrypted with whatever was typed |
| Every login after | Only the password from 20:45 works | n/a |
So I stopped chasing it. The reset backs up the old keyring, unlocks the placeholder with the account password, and stores a marker item straight away so the file is written there and then, with a password I know.
The FIDO2 tools only take a PIN from a terminal
fido2-cred and fido2-assert read the PIN from the controlling terminal and nowhere else. A script started at login has no terminal. So there is a small wrapper that runs the tool under a pseudo-terminal, waits for the word PIN in its output, and types the PIN in from a dialog. Twenty lines of Python. Not pretty, but it does the job and the PIN never touches a file or an argument list.
Test it somewhere it cannot hurt you
The most useful thing Claude Code did was refuse to trust the reset script on my real keyring. It ran the whole flow first against a second oo7-daemon on a private D-Bus session (dbus-run-session), with a throwaway home directory, a fake old keyring and a test password with £, @, " and # in it.
The first run failed. Good. The reset moved the old keyring to its backup, restarted the daemon, saw oo7’s placeholder login, took it for a real keyring and stopped. On my machine that would have left me with no keyring at all and a backup to restore by hand. The script now unlocks the placeholder instead of refusing it, and if anything fails after the backup is taken, it puts the old keyring back on its own.
Claude Code’s own safety check also stopped an earlier version of the reset on the real keyring until I had confirmed it. Annoying at the time. Reight call.
| Test, on the throwaway daemon | Result |
|---|---|
| Reset from a terminal prompt, old keyring backed up | Pass |
| Old password against the new keyring | Refused |
| Fresh daemon, as at login | Starts locked |
| Wrong password | Refused |
| Correct password, the same path a password login takes | Unlocks |
| Fresh daemon, PIN + touch through the unlock script | Unlocks |
| Unlock script with the keyring already open | Exits at once, no touch asked for |
Then once more on the real thing, with the keyring locked by hand and the unlock script run the way the login runs it: PIN, touch, open.
The Rough Edges
It is a working setup, not a finished product, and these are the bits I know about.
- Two PIN entries per key login. One at the login screen for PAM, one for the keyring. They are separate requests to the key and the key will not combine them. A touch-only keyring would save one, at the cost of the PIN protecting nothing after login.
- Change the account password and the stored copy goes stale. The unlock script says so in a notification rather than failing quietly. Run the reset again and it is sorted.
- Lose the key and you are back to typing the password. Nothing is lost, because the password is still the account password.
- Some apps rewrite their own autostart files. If one of them ever raises the prompt again, that is why, and the wrapper needs putting back.
- oo7 is a beta, and the unlock call goes through an interface with “Unsupported” in its name. Every update to oo7 is worth a test login before relying on it.
Close
The keyring was never broken. Neither was the key. They were built on different assumptions about what a login is, and nobody upstream joined them up. Passwordless login is documented by Yubico, shipped in Fedora’s authselect profiles and switched on with one feature flag. Nowt in any of it tells you that the moment you use it, every secret your desktop holds stays locked behind a password you are no longer typing.
As such the gap falls to whoever is sat at the machine, and the fix is only as good as that person’s understanding of where the secret actually lives. The easy answer, a blank password, is the one most people get pointed to, and it trades a dialog box for an unencrypted file of every token you own. That is not a fix. That is giving up quietly.
The right shape was there the whole time. The key can hold a secret, the standard says so in as many words, and the PIN can be part of that secret rather than a door in front of it. It took an evening and a test harness to wire it up. It should not have needed either.
freedesktop.org, Secret Service API Draft — the D-Bus API desktop apps use to store and fetch secrets, which both gnome-keyring and oo7 implement. ↩︎
Yubico, pam-u2f — “For a passwordless experience, where the authenticator PIN can be used in place of the user password”, with the example line
auth sufficient pam_u2f.so authfile=/etc/u2f_mappings cue pinverification=1. ↩︎FIDO Alliance, Client to Authenticator Protocol 2.1, section 12.5 HMAC Secret Extension — “This extension is used by the platform to retrieve a symmetric secret from the authenticator when it needs to encrypt or decrypt data using that symmetric secret”, and “The authenticator generates two random 32-byte values (called CredRandomWithUV and CredRandomWithoutUV)”. ↩︎ ↩︎
Yubico, libfido2 fido2-assert manual — the command-line tool used to request an assertion, with the hmac-secret, user presence and PIN options. ↩︎
linux-credentials/oo7 on GitHub — “server: org.freedesktop.secrets server implementation” and “pam: PAM integration for the server implementation”. ↩︎
oo7 source, server/src/gnome/internal.rs — oo7’s implementation of gnome-keyring’s internal unlock interface, including
UnlockWithMasterPassword. ↩︎