Skip to content

Hardware keys (YubiKey)

By default, murk init generates a raw age key and stores it under ~/.config/murk/keys/. That’s fine for development, but the private key exists as plaintext bytes on disk. Anyone with read access to the file can decrypt everything the key is authorized for.

murk doesn’t have a first-class “yubikey” flag or subcommand. Instead it supports age plugin identities: point MURK_KEY_FILE at a plugin identity file, and the private key material lives in tamper-resistant hardware and never exists as raw bytes murk (or anything else) can read directly. This is the same age plugin mechanism the wider age ecosystem uses, so any plugin that speaks it works with murk.

Hardware Plugin
YubiKey, Nitrokey, any PIV-capable smart card age-plugin-yubikey
Apple Secure Enclave (Touch ID) age-plugin-se
Any FIDO2 security key age-plugin-fido2-hmac
OpenPGP Card age-plugin-openpgp-card
Terminal window
# Install the plugin; it must be on $PATH, murk shells out to it on decrypt
brew install age-plugin-yubikey
# Generate an identity bound to the YubiKey. Prompts for the PIV PIN
# (default 123456) and a touch. --touch-policy always requires a physical
# tap for every decrypt; --pin-policy once caches the PIN per session.
age-plugin-yubikey --generate --touch-policy always --pin-policy once \
> ~/.config/murk/yubikey.txt
# murk refuses identity files readable by others
chmod 600 ~/.config/murk/yubikey.txt
# Point murk at the identity file
echo 'export MURK_KEY_FILE=~/.config/murk/yubikey.txt' >> .env
# Authorize the YubiKey's public key on your vault
murk circle authorize $(grep -i recipient ~/.config/murk/yubikey.txt | awk '{print $NF}')
# Read a secret: the YubiKey blinks; tap it to decrypt
murk get SOME_KEY

The identity file contains a # Recipient: age1yubikey1... header followed by an AGE-PLUGIN-YUBIKEY-1... pointer. murk reads the public key straight from that header (no plugin call needed just to look up a scoped secret) and only invokes the plugin binary when actually decrypting, which is when the YubiKey prompts you to tap it.

The same pattern applies to the other plugins in the table: generate an identity file with the plugin’s own CLI, point MURK_KEY_FILE at it, and authorize the pubkey it prints.

If the plugin binary isn’t on $PATH, murk fails with an age-plugin-<name> unavailable error. Install it and retry.

MURK_KEY (the inline environment variable, as opposed to MURK_KEY_FILE) rejects AGE-PLUGIN-... strings outright: a bare plugin pointer doesn’t carry the recipient public key murk needs, so hardware identities only work via MURK_KEY_FILE pointing at the identity file.

The entire point of a hardware-backed key is that the raw key bytes never leave the device, so there are no bytes to encode as a recovery phrase. murk restore and murk recover are age-key-only; running them against a plugin identity is a hard error, not a silent no-op.

The recovery strategy for a hardware key is different: enroll a second hardware device at setup and add both public keys as recipients (murk circle authorize <backup-pubkey>). If you lose one device, the backup still decrypts. Treat this the same way you’d treat a backup SSH key or a spare hardware security key for any other account: set it up before you need it, not after you’ve lost the primary.

age keys and ssh-ed25519 keys can produce a vault signature on write; hardware/plugin identities and ssh-rsa keys cannot (age exposes no signing scalar for them), so writes from a hardware-only identity leave the vault unsigned: a warning on next load, not a hard failure. A team entirely on hardware keys relies on git as the integrity anchor; a mixed team is signed whenever a signing-capable holder last wrote. See the threat model for the full signing story.

Setting MURK_KEY to a raw AGE-SECRET-KEY-1... string works, but the key is then plaintext in your environment/.env. Prefer MURK_KEY_FILE pointing at either a file under ~/.config/murk/keys/ (the murk init default, fine for development) or a hardware-backed plugin identity file (recommended for production use). See environment variables for the full list murk reads.