Give CI a short-lived identity
Workload identity, without a stored secret.
Written By Dustin
Last updated 27 minutes ago
A pipeline needs to authenticate without a long-lived token sitting in a secret store. TOVIO exchanges the OIDC token your CI provider already issues for a short-lived, repository-bound session.
On GitHub Actions, use the bridge
The TOVIO CI bridge action performs the enrolment for you as part of setup, taking the policy id and audience as inputs, so a job authenticates with no stored secret and no hand-rolled command sequence. Reach for that first. The rest of this article is what it does underneath — and what you run on a platform the bridge does not cover.
Trust the issuer once
tovio agent issuer-policy ci-main \ --issuer https://token.actions.githubusercontent.com \ --audience <audience> \ --subject <subject> \ --file ./ci-main.vexSigns a trust-domain policy naming which issuer, audience and subject may exchange tokens. --file is required — it is where the signed artifact is written.
The output is a public signed artifact. Deploy it into the served repository at .tovio/forge/runner-issuers/<policy-id>.vex. Never copy the owner key to a runner; the runner needs the policy, not your identity.
Keep --subject tight. It is matched exactly — there is no glob matching — and you repeat the flag for each identity you mean to allow, which is deliberately more work than writing a wildcard.
That --expires-in default catches people both ways. Set it deliberately: assume 24 hours and you leave a policy live for a week; leave it alone and forget, and CI runners start failing to exchange tokens seven days after setup with no obvious cause.
Exchange at run time
tovio agent session oidc <remote> <repository-id> \ --policy ci-main \ --id-token-file ./oidc.jwt \ --passphrase-file ./pass.txt \ --file ./session.bundle \ --task-id "$RUN_ID"All five flags are required. Both credentials are file references by design: the provider token is read once and zeroized, and the separate passphrase encrypts the runner key inside the output bundle. Neither value ever appears in argv, so neither lands in a process list or a shell history.
--file must not already exist. --ttl-seconds requests a lifetime, but the owner policy, the JWT's own expiry, and a 3600-second ceiling all cap it.
Where OIDC is not available
tovio agent session export <token-id> bundle.enc --passphrase-file pass.txtA passphrase-encrypted runner bundle carrying the public grant and the agent's signing key. Second best — it is a stored credential — so scope and expire it tightly; --expires-in is in hours and defaults to 1.
A runner selects the bundle and its passphrase together through TOVIO_AGENT_SESSION_BUNDLE_FILE and TOVIO_AGENT_SESSION_PASSPHRASE_FILE. If one is present later and the other is not, the command fails explicitly rather than quietly falling back to some human identity it found lying around.
Bundle problems
TVO-TOKEN-011 covers the whole family: absent on one side of the pair, empty, oversized, expired, built for another repository, encrypted under a different passphrase, tampered with, or no longer backed by a valid registration and capability chain. It fails closed in every one of those cases.
Clearance in CI
A step referencing a secret the job's token is not cleared for fails closed — TVO-CI-006 — detected before the step runs. The message names the secret and the step index and never a value. Never a silent pass, and never a job that runs with an empty variable where a secret should have been.