Unlock & the sealed bundle 

    The bundle is a 0600 file holding the backend credential, encrypted under a master key that is itself wrapped to one or more unlock slots. At startup Basil recovers the master key from whichever slot you supply, decrypts the credential, and zeroizes the key. It fails closed if no slot opens.

    What the bundle carries 

    The decrypted payload is a map of backend id → BackendCred. One credential per backend; the broker hands each one to the matching backend at startup, then zeroizes the whole map.

    CredentialBackend kindWhat it holds
    VaultTokenvaultA static bearer token: simplest, for dev or tightly controlled automation.
    VaultAppRolevaultA role_id + secret_id exchanged for a short-lived token at startup, the standard production choice.
    SpiffeSignervaultA private signing key the broker uses to self-mint a JWT-SVID at auth/<mount>/login. No static backend secret on disk.
    DbKeystoreDekkeystore (db-keystore)The 32-byte DEK that opens the encrypted local database.
    OnePasswordkeystore (1password)Provider URI, project, and profile for the 1Password materialize-to-use backend.
    AwsKmsaws-kmsAWS region and optional profile for the in-place AWS KMS transit backend.
    GcpKmsgcp-kmsGCP project, location, key ring, and optional sealed service-account JSON for the in-place GCP Cloud KMS transit backend.

    See Backends & capabilities for the full authentication detail and when to choose each credential kind.

    Unlock slots 

    SlotConfig keyNotes
    age / YubiKeyage-yubikey = trueMaster key wrapped to an age recipient; a YubiKey touch/PIN (via age-plugin-yubikey) recovers it. Strongest interactive production slot.
    Passphraseunlock-passphrase-file = "<FILE>"A production passphrase read from a 0600 file or systemd credential, Argon2id-stretched, then wiped by default after startup reads it.
    BIP39bip39-phrase-file = "<FILE>"A 24-word recovery phrase read from a 0600 file (never argv/env). Break-glass.
    TPMunlock-tpm = trueimplemented Master KEK sealed to host TPM 2.0 PCR state; unattended boot, no operator secret. Needs the unlock-tpm build.

    Set strict-bundle-perms = true to refuse startup if the bundle isn't 0600 (default is warn-only).

    For read-only credential mounts, set unlock-passphrase-no-wipe = true. Otherwise Basil attempts a best-effort overwrite and remove after it has read the passphrase into zeroizing memory.

    The TPM slot is available in a binary built with the non-default unlock-tpm feature. It seals the master KEK to the host's TPM 2.0 PCR state, so the host unlocks itself at boot with no operator secret. Create it with basil bundle create --slot tpm[:pcrs=0,2,4,7] (PCRs default to 0,2,4,7, hash bank sha256) and enable it with [unlock] unlock-tpm = true. A binary without the feature fails the slot closed. See Automated boot unlock.

    Choosing an unlock method 

    The right slot depends on what you are optimizing: operator presence, unattended boot, or emergency recovery. Rank them by the trust root you are willing to stand behind.

    RankMethodTrust rootMain risk
    1age-yubikeyHardware token plus operator PIN/touch; the private key never leaves the token.Token theft plus PIN compromise; availability depends on the token being present.
    2TPM (unlock-tpm build)A KEK bound to host TPM 2.0 PCR state.Host compromise; no operator presence once the measured state matches.
    3File-sourced passphraseThe passphrase file or systemd credential, Argon2id-stretched by the bundle slot.Reduces to how the file is protected; a fetcher adds its own standing token.
    4BIP39The phrase itself.If the phrase store leaks, the bundle is offline-attackable; use it for break-glass only.
    ✅ Automated boot unlock

    Use the passphrase slot when a service must start unattended. Basil stays source-agnostic: a systemd unit, 1Password op read, or another fetcher can write the passphrase to a file before basil agent starts. Scope and rotate that upstream token as if it can unlock the vault, because it can. See Automated boot unlock.

    Building & updating a bundle 

    # create a bundle with BIP39 break-glass, a passphrase slot, and an AppRole backend cred
    # (secret_id read from a 0600 file, never inline)
    basil bundle create /var/lib/basil/bundle.sealed \
      --slot bip39 \
      --slot passphrase:file=/run/secrets/basil-unlock-passphrase \
      --backend id=bao,type=openbao,addr=https://bao.example,role-id=ROLE_ID,secret-id-file=/run/secrets/approle-secret-id
    
    # rotate just the backend credential in the sealed payload
    basil bundle set-backend /var/lib/basil/bundle.sealed \
      --backend id=bao,type=openbao,addr=https://bao.example,role-id=NEW_ROLE_ID,secret-id-file=/run/secrets/new-secret-id \
      --open bip39:file=/run/secrets/breakglass.txt
    ⚠️ The BIP39 phrase is shown once

    bundle create --slot bip39 generates and prints the 24-word phrase a single time. Capture it then (offline, out of band): there's no way to recover it later, and it's your last way back in if the primary slot is lost. Secrets are always read from 0600 files, never inline arguments.

    Use bundle verify as a non-destructive preflight before a restart or before changing the source of a passphrase file:

    basil bundle verify /var/lib/basil/bundle.sealed \
      --open passphrase:file=/run/secrets/basil-unlock-passphrase

    Credential deposits 

    Credential deposits separate contributing one backend credential from opening the whole bundle. This is useful when a cloud administrator owns a credential such as a GCP service-account JSON, but should not receive the unlock secret that exposes every other backend credential.

    The bundle stores an X25519 ingest private key and contributor allow-list inside the sealed payload. The public recipient can be written at create time:

    basil bundle create /var/lib/basil/bundle.sealed \
      --slot passphrase:file=/run/secrets/basil-unlock-passphrase \
      --backend id=bao,type=openbao,addr=https://bao.example,token-file=/run/secrets/bao-token \
      --deposit-key /var/lib/basil/deposit.pub

    An admin allows a contributor signing key for one or more backend ids:

    basil bundle allow /var/lib/basil/bundle.sealed \
      --contributor <ed25519-public-token> \
      --backend gcp1 \
      --open passphrase:file=/run/secrets/basil-unlock-passphrase

    The contributor then appends a signed deposit without an unlock secret:

    basil bundle deposit /var/lib/basil/bundle.sealed \
      --backend id=gcp1,type=gcp-kms,project=PROJECT,location=global,key-ring=RING,key-file=/run/secrets/gcp-sa.json \
      -r /var/lib/basil/deposit.pub \
      -i /run/secrets/alice.ed25519.seed

    At startup, Basil unlocks the normal sealed payload first, verifies the allow-list and signature, and then overlays the newest authorized current-epoch deposit for that backend id. Invalid, stale, unauthorized, superseded, or undecryptable deposits are ignored for startup rather than crashing the broker.

    Review before committing deposits into the sealed payload:

    basil bundle promote /var/lib/basil/bundle.sealed --dry-run \
      --open passphrase:file=/run/secrets/basil-unlock-passphrase
    
    basil bundle promote /var/lib/basil/bundle.sealed --backend gcp1 \
      --open passphrase:file=/run/secrets/basil-unlock-passphrase

    show without --open lists only plaintext deposit metadata. show --open and promote --dry-run add authorization status and non-secret fingerprints. GCP service-account deposits display the service account identity fields when present; otherwise Basil prints a SHA-256 fingerprint of the serialized credential.

    📝 Anti-rollback

    The bundle carries a monotonic epoch, checked at unlock against an epoch sidecar file. Restoring an old bundle over a newer one is refused, so a credential you rotated out can't be silently reinstated from a backup.

    Where to go next