Skip to content

YubiKey ​

The yubikey provider encrypts values into fnox.toml with a key derived from a YubiKey's HMAC-SHA1 challenge-response. Encrypting and decrypting both need the same physical YubiKey; fnox set writes new ciphertext.

Config typeyubikey
StorageEncrypted in config
Writes (fnox set)Yes
RequiresA YubiKey with HMAC-SHA1 challenge-response on slot 1 or 2; libusb

Quick start ​

Configure HMAC-SHA1 challenge-response on a slot with YubiKey Manager (ykman otp chalresp), and install libusb (brew install libusb on macOS, libusb-1.0-0 on Debian and Ubuntu). Then create the provider:

sh
fnox provider add secure yubikey

Setup asks for the slot (default 2), generates a random challenge, verifies it with a tap, and writes the result to fnox.toml:

toml
[providers.secure]
type = "yubikey"
challenge = "a1b2c3..." # generated hex challenge, abbreviated
slot = "2"

Store a value, then use it. Each command asks you to tap the YubiKey:

sh
fnox set API_KEY --provider secure
fnox exec -- ./start.sh

The config is portable: on another machine, plug in the same YubiKey and it decrypts.

Authentication ​

fnox talks to the YubiKey over USB; there is no CLI or account. Each fnox process asks for one tap per provider and keeps the HMAC response in memory until it exits, so fnox exec resolves every secret from the provider with a single tap.

The provider needs interactive approval, so it cannot run in non-interactive mode or in CI. Use another provider for CI secrets.

Configuration ​

FieldRequiredDefaultDescription
challengeYesNoneHex-encoded challenge sent to the YubiKey, generated by setup.
slotYesNoneOTP slot configured for HMAC-SHA1: "1" or "2".

Both fields accept { secret = "NAME" }. Every provider also accepts auth_command and daemon_cache.

How values are stored ​

  1. fnox sends challenge to the configured slot. The YubiKey returns a 20-byte HMAC-SHA1 response.
  2. HKDF-SHA256 derives a 256-bit AES key from that response, using the provider name as context.
  3. AES-256-GCM encrypts the value. fnox stores base64(nonce || ciphertext || tag) as the secret's value.

The HMAC 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_yubikey) 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 YubiKey and mark them env = false. A credential lease uses them to call sts:AssumeRole, and only the short-lived credentials reach your command:

toml
[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"

Troubleshooting ​

YubiKey support requires libusb, but it is not installed ​

fnox loads libusb at runtime. Install it with brew install libusb (macOS), sudo apt install libusb-1.0-0 (Debian and Ubuntu), or sudo dnf install libusb1 (Fedora).

YubiKey HMAC-SHA1 challenge failed ​

Check that the YubiKey is plugged in, that its OTP interface is enabled, and that the configured slot has HMAC-SHA1 challenge-response set up. Retry with fnox check --all; fnox provider test does not contact the YubiKey.

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.

See also ​

  • FIDO2: hmac-secret alternative for any FIDO2 key, with a comparison table.
  • Credential leases: use hardware-protected credentials to mint short-lived ones.
  • Plugin support: use a YubiKey's PIV applet through age-plugin-yubikey instead.
MIT LicenseCopyright © 2026jdx.dev