OpenBAO Auto Unseal with YubiHSM
Why to Use an HSM
Manual Shamir unsealing gets old fast once you run more than a couple of OpenBAO (or HashiCorp Vault) nodes in a private infrastructure. You either keep shares floating around or build some fragile automation that still leaves the root keys in software configuration. Dedicated, network accessible HSM hardware is an objectively better approach.
Yubico's HSM is an affordable alternative to reduce risk and adhere to compliance and regulations. It is a small USB device that talks PKCS#11 over LAN via a host connector service. OpenBAO’s PKCS#11 seal (now available as the openbao-plugin-kms-pkcs11 plugin) can hand the master key to it for decryption and get automatic unseal on every restart, while manual unseal (as a break-glass procedure) is still possible via the generated recovery keys.
This post goes through and explains the following PoC setup that pieces the configuration together.
HSM Preparation
Install the YubiHSM SDK. This step is required to start the HSM connector service on the host machine, that exposes the HSM functionality over the network. All the other necessary components and libs are installed in the PoC image in the above repository.
Firstly generate a new TLS cert and key for the transit encryption on the Host machine:
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout /etc/yubihsm/connector.key \
-out /etc/yubihsm/connector.crt \
-subj "/CN=host.docker.internal" \
-addext "subjectAltName=DNS:host.docker.internal,DNS:localhost,IP:127.0.0.1"
Copy and trust the new connector certificate on the host system
sudo cp /etc/yubihsm/connector.crt /etc/ca-certificates/trust-source/anchors/
sudo update-ca-trust
Or on RHEL based change the trust location to: /etc/pki/ca-trust/source/anchors/
Plug the YubiHSM in and start yubihsm-connector service on the host with the following configuration.
# /etc/yubihsm-connector.yaml
cert: "/etc/yubihsm/connector.crt"
key: "/etc/yubihsm/connector.key"
enable-host-whitelist: false
listen: 0.0.0.0:12345
serial: ""
yubihsm-connector -c /etc/yubihsm-connector.yaml
# OR via Systemd after installing the YubiHSM SDK
systemctl start yubihsm-connector
Once it is up and running, check the status: https://localhost:12345/connector/status The browser or curl should connect to this without any issues since the TLS certs should have already been trusted.
Optionally reset the device so it is tarting clean for the initial setup.
Configure the HSM Auth and OpenBAO keys
Generate a proper admin auth key, delete the default one, then make a dedicated auth key for OpenBAO with only the decrypt capabilities it needs:
export ADMIN_PW=$(yubihsm-shell -a get-pseudo-random --count 16 -p password --authkey 1)
yubihsm-shell -a put-authentication-key \
-i 0x0002 \
-d all \
-c all \
--delegated all \
-l ADMIN \
--authkey 1 -p password \
--new-password=$ADMIN_PW
Delete the Initial Unsecure Auth Key
yubihsm-shell -a delete-object \
-i 0x0001 \
-t authentication-key \
--authkey 1 -p password
Generate a dedicated OpenBAO Auth Key, just for the decryption
export BAO_PW=$(yubihsm-shell -a get-pseudo-random --count 16 --authkey 2 -p $ADMIN_PW)
yubihsm-shell -a put-authentication-key \
-i 0x0003 \
-d 1 \
-c decrypt-pkcs,decrypt-oaep \
-l "BAO-Auth" \
--authkey 2 -p $ADMIN_PW \
--new-password=$BAO_PW
Generate the OpenBAO Seal Key
yubihsm-shell -a generate-asymmetric-key \
-i 0x00ff \
-l bao-seal-key-rsa \
-d 1 \
-c decrypt-oaep,decrypt-pkcs \
-A rsa4096 \
--authkey 2 -p $ADMIN_PW
OpenBAO Configuration
The PoC repository has a Containerfile that pulls the official OpenBAO UBI image and layers the YubiHSM SDK libraries on top. The important part for the integration is the yubihsm_pkcs11.conf file with the connector url.
# yubihsm_pkcs11.conf
connector=https://host.docker.internal:12345
The OpenBAO PKCS#11 seal configuration as it is in the openbao.hcl snippet:
# openbao.hcl
plugin "kms" "pkcs11" {
image = "ghcr.io/openbao/openbao-plugin-kms-pkcs11"
version = "v0.1.0"
binary_name = "openbao-plugin-kms-pkcs11"
sha256sum = "55245882727535579e710672f0eae1bcdddc846006db857baaa6e09e33d40faf"
}
seal "pkcs11" {
lib = "/usr/lib64/pkcs11/yubihsm_pkcs11.so" # Installed via the image build
token_label = "YubiHSM"
pin = "0003<BAO-AUTH-PASSWORD>" # !!! the auth key location and the Auth password is one concatenated string value !!!
key_id = "0x00ff"
key_label = "bao-seal-key-rsa"
mechanism = "0x0009" # CKM_RSA_PKCS_OAEP
rsa_oaep_hash = "sha1"
}
Bring it all up and initialize the DB:
podman-compose -f compose.yaml up -d
podman exec -it <container> bao operator init
After the init the following output should tell the recovery keys (store those safely) and the root token:
Recovery Key 1: 3NV6K62D************************8KG0NeMNsOsT
Recovery Key 2: pUATUuuk************************gaP0+hFwzEL+
Recovery Key 3: xuy9sXto************************aFIejQyHiqnv
Recovery Key 4: itGD6wC9************************UX1WnaXqZ8zk
Recovery Key 5: cCzbKJfi************************sgN3pBm0C1s
Initial Root Token: s.Gp************************jEyI
Success! Vault is initialized
Subsequent restarts of OpenBAO are now unsealing themselves as long as the YubiHSM is reachable.
The Encryption Loop
- Storage backend data is encrypted by OpenBAO’s data-encryption keys (keyring, symmetric Database Encryption Key / DEK)
- The keyring is encrypted by the root key (master key)
- The root key is encrypted (wrapped) by the HSM key
What happens on initialization (first seal / wrap)
- OpenBAO generates a fresh root key in memory
- Via PKCS#11:
- C_Login with the configured PIN (key_id + PAO_PW)
- Locate the key object by label
- C_EncryptInit + C_Encrypt (or wrap) of the plaintext root key
- Sends the plaintext root key to the HSM with the encryption mechanism parameters
- The HSM returns the ciphertext (encrypted / wrapped root key)
- OpenBAO writes that ciphertext into the storage backend
Why only the decrypt-pkcs and decrypt-oaep capabilites
Why is the YubiHSM BAO_PW key configured with only the ecrypt-pkcs and decrypt-oaep capabilities. The root key obviously needs to be encrypted somewhere during the initialization! It is because with asymmetric RSA the encrypt side never touches the private key!
The Initialization flow as follows:
- OpenBAO obtains the public key (via C_GetAttributeValue / C_FindObjects on the public object)
- It then calls C_EncryptInit + C_Encrypt using the public key
- Public-key encryption is pure math and can be performed entirely outside the HSM by the PKCS#11 module using only the public component
- No private-key capability is required, so the BAO_PW Authentication Key does not need encrypt-* capabilities configured in the HSM
What happens on unseal (auto-unseal)
- OpenBAO starts sealed, reads the ciphertext of the root key from storage.
- Via PKCS#11:
- Login + find key object
- C_DecryptInit + C_Decrypt
- Sends the ciphertext to the HSM with the encryption mechanism parameters
- The HSM returns the plaintext root key! That is why we need TLS to protect this in transit!
- OpenBAO uses the plaintext root key to decrypt the keyring/DEK, loads the DEK into memory, and discards the root key
- OpenBAO is now unsealed and can serve requests
No Shamir shares are involved for the unseal path itself; recovery keys exist only for administrative operations (root token generation, seal migration, etc.).
In short: the HSM only ever sees the root-key ciphertext on unseal. The wrapping key itself never leaves the HSM!
Practical Notes
- The connector must be reachable from the container (host.docker.internal works on most Podman/Docker setups)
- OpenSC (pkcs-tool cli) 0.27 and recent yubihsm-shell versions can segfault together; stick to the versions the Yubico SDK ships or use yubihsm-shell / yubihsm-manager for key management
- File storage is used in the demo for simplicity, swap for production
- The TLS cert is just a quick self signed one, configure a proper TLS cert and key with your PKI
- The HSM Connector whitelisting is disabled. Configure proper whitelist entries for increased network security when accessing the HSM
Go forth and tessellate!