Your YubiKey can't tell you which accounts it's registered to. keyfleet can.
keyfleet is a local-first CLI that keeps a small YAML ledger of your hardware security keys, the accounts they are registered to, and the credential type of each registration — then answers the questions the keys themselves can't:
- Which accounts have only one key registered?
- What breaks if I lose this key — and where do I go to de-register it?
- Which of my keys does that vendor advisory affect?
- How close is this key to its passkey capacity?
It stores no secrets (validation refuses anything that looks like one) and makes no network calls, ever — both enforced by tests.
FIDO2 has two kinds of credentials. Discoverable credentials (passkeys)
live on the key — it has a fixed number of slots, and tools can list them.
But the classic security-key registration — U2F, and most "add a security
key" 2FA flows — is non-discoverable: the key stores nothing per
account. The service keeps a credential ID that only your key can use, and
the key just answers challenges with it. No software can enumerate those
registrations from the hardware — not ykman, nothing — because the list
does not exist anywhere except across the services themselves.
So the only complete map of what your keys unlock is the one you maintain. Every vendor's advice is to register a backup key with every account; nothing tracks whether you actually did. keyfleet is that map, plus the checker that turns it into answers.
keyfleet runs straight from PyPI — nothing to install if you have
uv: every command works as uvx keyfleet …,
which is how this walkthrough writes them. Prefer a bare keyfleet? Run
uv tool install keyfleet (or pipx install keyfleet) once, then drop the
uvx prefix everywhere below.
1. Create your ledger folder — from any terminal, in any directory:
uvx keyfleet init ~/keyfleet-ledgerThis writes a fictional example ledger into ~/keyfleet-ledger (creating the
folder if needed) and gitignores keyfleet.yaml there, so a real ledger can
never be committed by accident.
2. Go there and make the ledger yours.
Windows (PowerShell):
cd ~/keyfleet-ledger
Copy-Item keyfleet.example.yaml keyfleet.yaml
notepad keyfleet.yamlmacOS / Linux:
cd ~/keyfleet-ledger
cp keyfleet.example.yaml keyfleet.yaml
nano keyfleet.yaml # or any editorReplace the fictional keys and accounts with your own — The ledger below explains every field, and editors autocomplete from the bundled JSON schema. Start small: two keys and your three most important accounts already pays for itself.
3. Validate, then check. Run these from inside the folder — both commands
find keyfleet.yaml there on their own:
uvx keyfleet validate
uvx keyfleet checkvalidate proves the file is well-formed; check is the payoff:
keyfleet check — 4 keys (2 active, 1 spare, 1 lost) · 5 accounts
FAIL T0 "Primary email (Google)" has 1 hardware key registered; policy requires 3
FAIL Key yk-old is LOST but still registered on 3 accounts → run: keyfleet lost yk-old
WARN T0 "Primary email (Google)" lists sms as a factor
INFO T1 "Code hosting (GitHub)" has no recovery-code pointer (policy requires recovery codes for T1)
2 fail, 1 warn, 1 info · exit 1
Fix what it flags, re-run, repeat. From any other directory, pass the ledger
file (not the folder): uvx keyfleet check ~/keyfleet-ledger/keyfleet.yaml.
4. The day a key goes missing:
uvx keyfleet lost yk-oldThis prints the incident checklist: every affected account ordered by tier (the ones that become inaccessible first), which registration nickname to delete, and the service's security-settings URL — straight from the bundled, source-cited services table.
Two gotchas worth knowing: uvx caches tools, so right after a new keyfleet
release run it once as uvx --refresh keyfleet … to pick up the update; and
hacking on a clone is uv sync --all-extras --dev, then
uv run keyfleet check keyfleet.example.yaml.
Every command reads keyfleet.yaml from the current directory unless you pass
LEDGER (a file path). Running via uvx? Prefix each with uvx , e.g.
uvx keyfleet check.
| Command | What it does |
|---|---|
keyfleet validate [LEDGER] |
Schema + referential integrity + secret rejection. Exit 0/1/2. |
keyfleet check [LEDGER] [--json] |
Policy violations and coverage gaps; exit 1 on any FAIL. |
keyfleet lost KEY_ID [LEDGER] [--md] |
Impact analysis + ordered de-registration checklist. |
keyfleet report [LEDGER] [--md|--json] |
Coverage matrix, per-tier summary, key utilization. |
keyfleet advisories [LEDGER] |
Keys matching vendor advisories by firmware range. |
keyfleet services [--search NAME] |
The bundled service table. |
keyfleet init [DIRECTORY] |
Example ledger + .gitignore entry in DIRECTORY (default: here). |
The checks: minimum keys per account tier (default T0:3, T1:2, T2:1 — override per ledger), registrations still sitting on lost/retired keys, weak factors on sensitive tiers (default: sms/email on T0), spares registered nowhere, missing recovery-code pointers (never the codes), passkey-slot usage against known model capacities, and service-id typos.
keys:
- id: yk-blue
label: "Blue YubiKey 5C NFC — daily carry"
vendor: yubico
model: "YubiKey 5C NFC"
firmware: "5.7.1"
status: active # active | spare | lost | retired
accounts:
- id: email-primary
service: google # keys into the bundled services.yaml
label: "Primary email (Google)"
tier: T0 # T0 root of trust · T1 important · T2 nice-to-have
registrations:
- { key: yk-blue, type: fido2-discoverable, nickname: blue-daily }
other_factors: [totp-app, recovery-codes]
recovery_codes: { stored: true, where: "sealed envelope" } # a pointer, never the codesFull shape: keyfleet.example.yaml. Editors get
completion from schema/keyfleet.schema.json
(VS Code: map it to keyfleet*.yaml under yaml.schemas).
services.yaml ships knowledge about 32
services: where the security-key settings live, the documented maximum number
of keys, and whether passkeys are supported — every fact read from the
service's own page, with source_url and a verified date, and null
where the vendor documents nothing (never a guess). Browse it in
docs/SERVICES.md; add your service via
CONTRIBUTING.md — those PRs are the easiest way to help.
The ledger reveals which accounts exist and which keys guard them — treat it
as sensitive. Keep it as a password-manager document, in a private repo, or
age-encrypted: every command transparently
reads keyfleet.yaml.age (decrypted to memory only, never to disk). Set
KEYFLEET_AGE_IDENTITY to an identity file for non-interactive use.
- No secrets, ever: validation refuses fields or values that look like recovery codes, TOTP seeds, PINs, or OTP secrets, with tests to keep it so.
- No network calls in runtime code (a test greps the imports), no telemetry.
keyfleet.yamlis gitignored here and bykeyfleet init.- See SECURITY.md for reporting.
- v0.2 —
keyfleet add key|accountinteractive prompts; CSV import helpers. - v0.3 — optional read-only
ykman/fido2integration to pull model/firmware/serial; a static, local-storage-only PWA on the same schema.
Apache-2.0 — see LICENSE.
