Skip to content

YubiKey Setup Guide

This guide walks through programming a YubiKey for use with the unseal ceremony, setting up Linux USB access, and registering with the signer. Once configured, operators can unseal without typing a passphrase — the YubiKey computes the HMAC-SHA1 response automatically when touched.

YubiKey authentication only applies when using the DynamoDB key source with unseal mode. Filesystem keystores don’t use the seal/unseal system.

  • YubiKey 5 series — any form factor (USB-A, USB-C, Nano, NFC). The HMAC-SHA1 challenge-response feature is available on all YubiKey 5 models.

  • ykman — the YubiKey Manager CLI. Install it with your package manager:

    Terminal window
    # Debian/Ubuntu
    sudo apt install yubikey-manager
    # Fedora/RHEL
    sudo dnf install yubikey-manager
    # macOS
    brew install ykman
  • containment-chamber on your $PATH

  • A signer already initialized in unseal mode — see Seal & Unseal Guide


YubiKeys have two OTP slots. Slot 1 is typically used for Yubico OTP (the default factory configuration). Use slot 2 for HMAC-SHA1 challenge-response to avoid overwriting the factory credential.

Generate a random 20-byte HMAC secret and program it into slot 2:

Terminal window
ykman otp chalresp --generate 2

ykman generates a cryptographically random secret and programs it directly — you never see the raw bytes. The key is stored in the YubiKey’s secure element and cannot be extracted.

If you want to program a specific secret (for example, to clone to a backup key), use:

Terminal window
ykman otp chalresp --touch 2 <hex-secret>

The --touch flag requires a physical touch before responding to any challenge. This prevents malware from silently using the YubiKey while it’s plugged in. Recommended for production.


A single YubiKey is a single point of failure. If it’s lost or damaged, that operator’s share is permanently inaccessible. With a 2-of-3 threshold, losing one operator’s share still allows unsealing — but losing two does not.

To program a backup key with the same secret:

Option A: Program both keys at the same time (recommended)

Before running ykman otp chalresp --generate 2, decide how many keys you want. Generate the secret yourself and program each key:

Terminal window
# Generate a random 40-char hex secret (20 bytes)
SECRET=$(openssl rand -hex 20)
# Program primary key (insert it now)
ykman otp chalresp --touch 2 "$SECRET"
# Swap to backup key, then program it
ykman otp chalresp --touch 2 "$SECRET"
# Securely erase the secret from your shell
unset SECRET

Option B: Export from an existing key

YubiKeys don’t allow secret export — this is by design. If you didn’t save the secret when programming, you can’t clone the key. You’d need to re-register the operator with a new key pair.

Security tradeoff: Programming multiple keys with the same secret means any of them can unseal. The threshold still applies at the signer level (you still need N operators), but within a single operator’s share, either key works. This is the right tradeoff for most teams — the alternative is losing access entirely if a key is damaged.


Verify the key responds correctly before registering with the signer:

Terminal window
ykman otp chalresp 2 "test"

You should see a 40-character hex string (20 bytes, HMAC-SHA1 output). If you programmed with --touch, the key will blink — touch it to get the response.

If you get an error, check:

  • The key is inserted and recognized (ykman list)
  • Slot 2 is configured (ykman otp info)
  • You have USB access (see Step 4 for Linux udev setup)

On Linux, USB HID devices require either root access or a udev rule granting access to your user. Without this, ykman and containment-chamber will fail with a permission error.

Create a udev rule for YubiKey:

Terminal window
sudo tee /etc/udev/rules.d/70-yubikey.rules > /dev/null << 'EOF'
# YubiKey
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0010", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0110", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0111", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0114", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0116", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0401", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0403", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0405", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0407", GROUP="plugdev", MODE="0660"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0410", GROUP="plugdev", MODE="0660"
EOF

Add your user to the plugdev group and reload udev:

Terminal window
sudo usermod -aG plugdev "$USER"
sudo udevadm control --reload-rules
sudo udevadm trigger

Log out and back in for the group change to take effect. Then re-insert the YubiKey and test:

Terminal window
ykman list

You should see your key listed without needing sudo.


Once the signer is initialized and you have your registration token, register your YubiKey credential:

Terminal window
containment-chamber operator register \
--operator alice \
--registration-token "$ALICE_TOKEN" \
--yubikey \
--signer-url https://signer.example.com

The CLI sends a challenge to the YubiKey, reads the HMAC-SHA1 response, and uses it as the passphrase for Argon2id key derivation. If you programmed with --touch, the key will blink — touch it when prompted.

The response is:

{"registered": 1, "remaining": 2}

Once all operators register, the signer transitions to sealed. Proceed to the unseal ceremony.


After a restart, once the signer reaches kms_unsealed, each operator submits their share:

Terminal window
containment-chamber operator unseal \
--operator alice \
--yubikey \
--signer-url https://signer.example.com

The CLI challenges the YubiKey with a domain-separated challenge (containment-chamber:{operator}), reads the HMAC-SHA1 response, and submits it as the passphrase. Touch the key when it blinks.

While below threshold:

{"shares_received": 1, "shares_required": 2}

Once the threshold is reached:

{"status": "unsealed", "validators_loaded": 42}

Signing is now available.


The HMAC-SHA1 response from the YubiKey acts as the operator’s passphrase. Here’s how it flows through the system:

Diagram

What the YubiKey protects: The HMAC secret never leaves the YubiKey’s secure element. An attacker who steals your laptop can’t extract the secret — they’d need the physical key. Combined with the unseal threshold (e.g., 2-of-3 operators), a single compromised machine doesn’t unseal the signer.

What it doesn’t protect: If an attacker has both your laptop and your YubiKey, they can unseal as you. Physical security of the key matters. The --touch flag adds a layer: the key won’t respond without a physical touch, so malware can’t silently use it while it’s plugged in.

In transit: The HMAC response is sent to the signer over HTTPS. It’s never logged. On the server side, Argon2id (m=64MB, t=3) derives a key from the response, which is used to decrypt the operator’s Shamir share. The response itself is zeroized from memory after use.

Challenge domain separation: The challenge is containment-chamber:{operator} — different for each operator name. This means alice’s YubiKey response can’t be replayed as bob’s, even if they use the same physical key (which they shouldn’t).


“No YubiKey device found”

The key isn’t detected. Check:

  • The key is physically inserted
  • ykman list shows the device (if not, check USB connection)
  • On Linux, the udev rule is in place and you’re in the plugdev group (Step 4)
  • Try a different USB port

“Permission denied” on Linux

The udev rule isn’t active yet. Run:

Terminal window
sudo udevadm control --reload-rules && sudo udevadm trigger

Re-insert the key. If still failing, verify your user is in plugdev:

Terminal window
groups "$USER"

Log out and back in if the group was just added.

“Slot 2 is not configured”

The YubiKey hasn’t been programmed for HMAC-SHA1 challenge-response. Run Step 1 to program it.

Timeout waiting for touch

The CLI waits 10 seconds for a touch. If you don’t touch the key in time, the operation fails. Run the command again and touch the key promptly when it starts blinking.

“Wrong passphrase” / share decryption fails

The YubiKey response doesn’t match what was registered. This can happen if:

  • You’re using a different YubiKey than the one used during registration
  • The key was reprogrammed after registration
  • You’re using the wrong slot

If you’re certain you have the right key, run operator unseal-reset to clear partial shares and try again:

Terminal window
containment-chamber operator unseal-reset \
--auth-token "$AUTH_TOKEN" \
--signer-url https://signer.example.com

If the key was lost or reprogrammed, the operator’s share is inaccessible. As long as the remaining operators can meet the threshold, unsealing still works. To restore the lost operator, run an operator rotation — see Seal & Unseal Guide.

Multiple YubiKeys inserted

If more than one YubiKey is plugged in, the CLI picks the first one found. Insert only the key you want to use, or pass --yubikey-serial <serial> to target a specific device. Find the serial with ykman list.