OpenBao auto-unseal

OpenBao's seal "transit" stanza unseals OpenBao with a Transit key on another server. Point it at an edgekms tenant and an OpenBao that restarts unseals itself, with no one holding unseal shares and no cloud KMS. OpenBao has edgekms encrypt the key that protects its storage once, at initialization, and decrypt it at every start.

The seal holds a static token, so this guide mints a revocable token that can use one key and do nothing else.

1. A key, a policy, and the right to mint tokens for it

Add a key for the seal and a policy that can encrypt and decrypt with it. A minted token can only carry policies its minter holds, so the role you mint from (here the default owner role, which also holds admin and so token-create) needs the seal policy as well:

{
  "keys": {
    "openbao-seal": { "id": "k_c27f05e9a14d3b68", "latest_version": 1, "min_decryption_version": 1 }
  },
  "roles": {
    "owner": {
      "mount": "github-user",
      "github_user_ids": [1234567],
      "policies": ["admin", "default-all", "openbao-seal"],
      "token_ttl_secs": 3600
    }
  },
  "policies": {
    "openbao-seal": { "keys": { "openbao-seal": ["encrypt", "decrypt"] } }
  }
}

Keep your own GitHub id in github_user_ids; edgekms tenant config get shows it. The owner's own tokens can then also encrypt and decrypt with the seal key, which is the cost of minting from that role. A role made only for minting, with admin and openbao-seal and nothing else, avoids it.

2. Mint the seal token

Log in to the tenant, then call auth/token/create:

export VAULT_ADDR=https://acme.edgekms.dev
export EDGEKMS_GITHUB_CLIENT_ID="$(curl -fsS https://api.edgekms.dev/v1/info | jq -r .github_client_id)"
export VAULT_TOKEN="$(edgekms login github --addr "$VAULT_ADDR" --role owner)"

curl -fsS -X POST "$VAULT_ADDR/v1/auth/token/create" \
  -H @<(printf 'X-Vault-Token: %s\n' "$VAULT_TOKEN") \
  -d '{"policies": ["openbao-seal"], "ttl": "8760h", "display_name": "openbao-seal"}' |
  jq '{token: .auth.client_token, accessor: .auth.accessor}'

Keep the accessor; it's how you revoke the token. A minted token never carries admin, whatever its minter holds, so a stolen seal token can unseal that one OpenBao and do nothing else.

The longest TTL is 9600 hours (400 days). The token can't be renewed: OpenBao's seal calls renew-self at startup, logs the error once and carries on, as it does for any non-renewable token. Mint a new token and swap it into the config before the old one expires, or OpenBao won't unseal after its next restart.

3. Configure the seal

seal "transit" {
  address    = "https://acme.edgekms.dev"
  token      = "<the token above>"
  key_name   = "openbao-seal"
  mount_path = "transit/"
}

Initialize a new OpenBao with recovery keys instead of unseal keys (bao operator init -recovery-shares=1 -recovery-threshold=1, or more shares as your policy needs); an existing Shamir-sealed OpenBao moves to the transit seal with bao operator unseal -migrate. From then on it unseals on every start.

Revoking and rotating

To cut the seal off, revoke the token by its accessor. Every edge location refuses it within about a minute, and OpenBao stays sealed at its next restart:

curl -fsS -X POST "$VAULT_ADDR/v1/auth/token/revoke-accessor" \
  -H @<(printf 'X-Vault-Token: %s\n' "$VAULT_TOKEN") \
  -d '{"accessor": "<accessor>"}'

A minted token carries its minter's role, so raising that role's not_before also invalidates every seal token it minted; mint new ones afterwards.

Rotating openbao-seal (raising latest_version) is safe: what OpenBao stored stays decryptable under its old version until you raise min_decryption_version past it. Don't raise it until OpenBao has rewritten its sealed key under the new version.