Keys on a headless or CI machine

TOVIO_KEYSTORE_KEY, and the two failures it causes when you get it wrong.

Written By Dustin

Last updated About 3 hours ago

A CI runner or a headless server has no Keychain, no Secret Service and no Credential Manager. Since that is where TOVIO normally keeps the 32-byte wrapping key your identity files are sealed under, operations that need a key fail with TVO-KEY-002 — key material missing.

The fix

Inject the wrapping key out of band, through TOVIO_KEYSTORE_KEY or its _FILE variant. Prefer the file form: an environment variable is visible to every process in the job, and often to the build log.

The value must be 64 hexadecimal characters — 32 bytes. Provision it as a secret in your CI system. It is a wrapping key for real identity material: treat it exactly as you would a signing key, not as configuration.

You can also force the choice explicitly with TOVIO_KEYSTORE: os to use the platform store, headless to require the injected key, or auto (the default, and what you get when it is unset) to use the injected key when one is present and the platform store otherwise.

A malformed key is refused, not ignored

If TOVIO_KEYSTORE_KEY is set but is not 64 hex characters, TOVIO refuses to run rather than quietly falling back to the OS keychain. A silent fallback would seal your keys under a key you did not choose, which is the sort of thing you discover months later. Fix the value or unset it — or set TOVIO_KEYSTORE=os if the platform store really is what you want.

Failure one: sealed under the wrong key

Key files sealed under this machine's real keychain will not open under an injected key, and vice versa. The error says which case you are in. Seal and open consistently: either the runner always uses the injected key, or it always uses the platform store — never a mix across jobs.

Failure two: the right key, the wrong identity

The injected key deliberately wraps every account, because a runner holds one secret for everything. So possessing the wrapping key is not evidence of which identity you are. TOVIO closes that gap by re-deriving the public key from the unsealed secret and comparing it to .tovio/identity.pub. A mismatch is TVO-KEY-007 and TOVIO refuses to sign or decrypt at all.

In practice this catches a renamed or copied key file. Do not move .key files between slots to "fix" a CI problem — that is the exact substitution the check exists to stop.

Give CI its own identity

Do not copy a developer's identity onto a runner. Enrol the runner as its own device, or issue it a scoped capability token, and grant it only the attributes it needs:

tovio access grant --identity ./ci-runner.identity.pub --attr role=ci

Then write policies against role=ci. Revoking a runner later is then one command and does not disturb anybody's laptop.

Do not let it read what it does not need

A build almost never needs to decrypt production secrets. Grant the narrowest attribute set that makes the build pass, and check it:

tovio access check <path> --identity ./ci-runner.identity.pub

Non-interactive init writes your recovery phrase to a file

On a piped or --json run, tovio init cannot print a phrase for someone to read, so it writes it to .tovio/tovio-recovery-key.txt and reports the path. On a throwaway runner that file disappears with the container. If the repository matters, capture the phrase from that first run or create the identity somewhere durable instead.

For the same reason, be careful what a failing job prints. Never echo TOVIO_KEYSTORE_KEY to debug it, never let a job dump .tovio/, and when you share a CI log, strip the key, any DIDs, and the recovery-phrase path before it leaves your organisation.