FIDO2
The fido2 provider encrypts values into fnox.toml with a key derived from a FIDO2 security key through the CTAP2 hmac-secret extension. Encrypting and decrypting both need the same physical key; fnox set writes new ciphertext.
| Config type | fido2 |
| Storage | Encrypted in config |
Writes (fnox set) | Yes |
| Requires | A FIDO2 key with hmac-secret support, plugged in |
FIDO2 support alone is not enough: confirm that your key and firmware support the hmac-secret extension. The provider is not included in Linux musl builds of fnox.
Quick start
Plug in the key and create the provider:
fnox provider add secure fido2Setup asks for the key's PIN only if the key has one (a fingerprint or touch-only key gets no PIN prompt), creates a credential with hmac-secret enabled, generates a random salt, and verifies the key with a second touch. It writes the result to fnox.toml:
[providers.secure]
type = "fido2"
credential_id = "a1b2c3..." # generated hex credential ID, abbreviated
salt = "d4e5f6..." # generated 32-byte hex salt, abbreviated
rp_id = "fnox.secure"Store a value, then use it. Each command asks you to touch the key:
fnox set API_KEY --provider secure
fnox exec -- ./start.shThe config is portable: on another machine, plug in the same key and it decrypts.
Authentication
fnox talks to the key directly; there is no CLI or account. Each fnox process asks for one touch per provider and keeps the hmac-secret response in memory until it exits, so fnox exec resolves every secret from the provider with a single touch.
fnox asks the key how it verifies you and prompts to match:
| Key setup | What fnox asks for |
|---|---|
| Fingerprint enrolled (for example, YubiKey Bio) | Your fingerprint. No PIN prompt; if the read fails in a terminal, fnox offers the PIN. |
| PIN set, no fingerprint | The PIN, then a touch. |
| No PIN, no fingerprint | A touch only. |
fnox provider add asks for a PIN only when the key has one, and does not store it. With no terminal and a PIN-protected key, set pin as described in Avoid typing the PIN.
The provider needs interactive approval, so it cannot run in non-interactive mode or in CI. Use another provider for CI secrets.
Fewer prompts
- Turn on the daemon. After one touch, later reads in your session come from memory instead of asking for another. A cache miss still needs a touch in a terminal.
- Use a lease. The key is touched when a lease is created; cached leases are reused without a touch.
- Encrypt most values with age. Keep one age identity behind the key (see Plugin support) and encrypt the rest of the project with age.
Avoid typing the PIN
pin accepts a secret reference, so the PIN can live in the OS keychain instead of being typed on each run. Keep the PIN provider and secret in your gitignored fnox.local.toml:
[providers.keychain]
type = "keychain"
service = "fnox"
[secrets]
FIDO2_PIN = { provider = "keychain", env = false }
[providers.secure]
type = "fido2"
credential_id = "a1b2c3..."
salt = "d4e5f6..."
rp_id = "fnox.secure"
pin = { secret = "FIDO2_PIN" }The key is still touched on each use. Never put the PIN itself in a committed fnox.toml.
Configuration
| Field | Required | Default | Description |
|---|---|---|---|
credential_id | Yes | None | Hex-encoded credential ID, generated by fnox provider add. |
salt | Yes | None | Hex-encoded 32-byte salt sent to the key, generated by fnox provider add. |
rp_id | Yes | None | Relying party ID of the credential. Setup uses fnox.<provider-name>. |
pin | No | None | Key PIN. When unset, fnox prompts on a terminal only if the key needs one. |
All fields accept { secret = "NAME" }. Every provider also accepts auth_command and daemon_cache.
How values are stored
- fnox sends
saltto the key during an assertion forcredential_id. The key returns a 32-byte secret derived from its internal device secret. - HKDF-SHA256 derives a 256-bit AES key from that response, using the provider name as context.
- AES-256-GCM encrypts the value. fnox stores
base64(nonce || ciphertext || tag)as the secret'svalue.
The hmac-secret response is never written to disk.
Renaming providers invalidates ciphertext
The provider name is part of key derivation. Renaming a provider (for example, from secure to my_fido2) changes the derived key, making all previously encrypted secrets and cached lease credentials undecryptable. To migrate, keep the old provider available, create a new provider under the new name, and read values through the old provider before storing them with the new one. Verify the new values before removing the old configuration.
Protect lease credentials
Store long-lived master credentials with the key and mark them env = false, so only a credential lease uses them and only short-lived credentials reach your command:
[secrets]
AWS_ACCESS_KEY_ID = { provider = "secure", env = false }
AWS_SECRET_ACCESS_KEY = { provider = "secure", env = false }
[leases.aws]
type = "aws-sts"
role_arn = "arn:aws:iam::123456789012:role/dev-role"
region = "us-east-1"FIDO2 or YubiKey
| Feature | fido2 | yubikey |
|---|---|---|
| Key types | Any FIDO2 key | YubiKey only |
| Protocol | CTAP2 hmac-secret | HMAC-SHA1 challenge-response |
| PIN support | Yes | No |
| Key output | 32 bytes | 20 bytes |
Choose fido2 if your key supports hmac-secret. Choose yubikey if you use a YubiKey OTP slot configured for HMAC-SHA1 challenge-response.
Troubleshooting
FIDO2 authenticator does not support hmac-secret extension
The key created a credential but did not enable hmac-secret. Use a key and firmware that support the extension.
Failed to find FIDO2 device
fnox found no FIDO2 key. Plug the key in, close other applications that hold it open, and retry with fnox check --all. fnox provider test does not contact the key.
requires interactive authentication and cannot be used in non-interactive mode
The command ran with --non-interactive or FNOX_NON_INTERACTIVE set, or from shell integration, the MCP server, or the TUI. Run fnox exec in a terminal instead.
needs the key's PIN but there is no terminal to ask on
The key has a PIN and fnox cannot prompt. Set pin = { secret = "NAME" } as in Avoid typing the PIN, or run the command in a terminal.
See also
- YubiKey: HMAC-SHA1 challenge-response alternative.
- Credential leases: use hardware-protected credentials to mint short-lived ones.
- Plugin support: use a FIDO2 key through an age plugin instead.