Workload identity
Let CI jobs and pods authenticate with the OIDC identity their runtime already gives them — no static Vaultic token anywhere.
Every secrets manager has to answer the same awkward question: to fetch your secrets, you first
need a secret. The usual answer is a long-lived VAULTIC_TOKEN pasted into GitHub repo secrets or
a Kubernetes Secret — a credential that is rarely rotated, easily copied between environments,
and outside Vaultic's control the moment it leaves.
Workload identity federation removes it. Every runtime you already run CI in hands the job a cryptographically verifiable identity: GitHub Actions issues a signed OIDC token to every job, Kubernetes projects a ServiceAccount JWT into every pod, GitLab CI issues one per job. Register the issuer with Vaultic, describe which claims may assume which scope, and the job exchanges its own token for a short-lived Vaultic credential.
Configuring providers and trust rules requires the Team plan. Static service tokens keep working on every plan — this is additive, and stays the right choice for local scripts, unsupported runtimes, and break-glass access.
How it works
Register a provider
A provider is one OIDC issuer this workspace trusts. Presets fill in everything that doesn't vary: for GitHub Actions the only real question is which repository.
Add trust rules
A trust rule maps claims on the incoming token — repository, ref, sub, whatever the
issuer sends — to exactly the scope a service token would carry: a project, an environment, read
or write, and a token lifetime.
The job exchanges its token
POST /auth/workload verifies the token's signature against the issuer's JWKS, finds the first
matching rule, and returns a Vaultic access token scoped to that rule and expiring in minutes. The
CLI does this transparently.
Nothing downstream changes. The exchanged token is an ordinary scoped Vaultic credential, subject to the same project/environment checks, the same trusted-IP allowlists, and the same audit log as everything else — with the workload's own claims recorded alongside each entry.
GitHub Actions
Create the provider (the issuer URL is filled in for you):
vaultic workload-identity create github --kind github-actionsThat prints a globally-unique provider slug — acme-github or similar. That slug is what jobs
authenticate against, and it is not a secret.
Add a rule. This one lets any workflow run on main in one repository read production:
vaultic workload-identity rules add acme-github prod-read \
--claim "repository=acme/backend-api" \
--claim "ref=refs/heads/main" \
--env production --read-only --ttl 15mThen in the workflow — note there is no secrets.VAULTIC_TOKEN anywhere:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # required — without it GitHub won't mint an OIDC token
contents: read
env:
VAULTIC_WORKLOAD_PROVIDER: acme-github
steps:
- uses: actions/checkout@v4
- run: npm install -g @vaultic/cli
- run: vaultic run --env production -- node deploy.js$VAULTIC_WORKLOAD_PROVIDER is enough on its own: any command needing credentials detects the
ambient OIDC token, exchanges it, and caches the result in memory for the life of the process. Add
an explicit vaultic login --workload step if you'd rather a misconfiguration fail at the top of
the job than at whichever command first needed a secret.
Available claims
GitHub sends a rich token. The ones worth writing rules against:
| Claim | Example | Notes |
|---|---|---|
repository | acme/backend-api | The one almost every rule should pin. |
repository_owner | acme | Too broad on its own — anyone in the org matches. |
ref | refs/heads/main | Branch or tag the run is on. |
environment | production | Only present when the job declares a GitHub environment. |
workflow | deploy | Workflow name, not path — renaming it breaks the rule. |
actor | octocat | Who triggered the run. |
Kubernetes
Point the provider at your cluster's OIDC issuer:
vaultic workload-identity create prod-cluster \
--kind kubernetes \
--issuer https://oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLERules match the ServiceAccount through sub:
vaultic workload-identity rules add prod-cluster backend-api \
--claim "sub=system:serviceaccount:production:backend-api" \
--env production --read-onlyThe pod needs a projected ServiceAccount token with vaultic as its audience — the default
token mounted into every pod is audienced for the API server and will be rejected:
spec:
serviceAccountName: backend-api
containers:
- name: app
env:
- name: VAULTIC_WORKLOAD_PROVIDER
value: acme-prod-cluster
- name: VAULTIC_WORKLOAD_TOKEN_FILE
value: /var/run/secrets/vaultic/token
volumeMounts:
- name: vaultic-token
mountPath: /var/run/secrets/vaultic
readOnly: true
volumes:
- name: vaultic-token
projected:
sources:
- serviceAccountToken:
path: token
audience: vaultic
expirationSeconds: 3600Clusters the Vaultic server can't reach
Self-hosted control planes usually aren't publicly reachable, so server-side OIDC discovery fails. Paste the JWKS document instead:
kubectl get --raw /openid/v1/jwks > jwks.json
vaultic workload-identity create prod-cluster \
--kind kubernetes \
--issuer https://kubernetes.default.svc \
--jwks-file jwks.jsonPasted keys are pinned: they are never auto-refreshed, so re-paste them if the cluster's signing keys are ever rotated.
GitLab CI
vaultic workload-identity create gitlab --kind gitlab-ci
vaultic workload-identity rules add acme-gitlab prod \
--claim "project_path=acme/backend-api" \
--claim "ref=main" \
--env production --read-onlyDeclare the token in the job with the matching audience — the CLI reads $VAULTIC_ID_TOKEN:
deploy:
id_tokens:
VAULTIC_ID_TOKEN:
aud: vaultic
variables:
VAULTIC_WORKLOAD_PROVIDER: acme-gitlab
script:
- vaultic run --env production -- ./deploy.shWriting trust rules
Every predicate in a rule must match. Three operators, deliberately no regex — regex in a trust
policy is how repository == acme/app.* ends up trusting acme/app-fork-by-attacker.
| CLI syntax | Operator | Matches |
|---|---|---|
--claim "repository=acme/api" | exact | that value and nothing else |
--claim "ref~=refs/heads/*" | glob | * matches one path segment |
--claim "ref@=refs/heads/main,refs/heads/release" | one of | any listed value |
A glob never crosses a /. refs/heads/* matches refs/heads/main but not
refs/heads/feature/login — and not refs/heads/main/../evil either, which is the point. Write
a rule per branch shape rather than reaching for a wildcard that swallows extra path segments.
Rules are evaluated in creation order and the first fully-matching rule wins. A broad rule created first shadows narrower ones created after it, so add the specific rules first.
A rule grants exactly one project/environment scope, like a service token does. A deploy job that needs staging and production needs two rules and two exchanges.
Debugging a rule that doesn't match
This is the normal failure mode, and the exchange endpoint deliberately won't help an unauthenticated caller debug it: a 403 lists which claim names the token carried, never their values. Run the dry run inside the job instead — it reports what would happen and mints nothing:
vaultic workload-identity test --provider acme-githubWould match rule "prod-read" in workspace "acme"
access: read, backend-api/production
matched on:
repository = acme/backend-api
ref = refs/heads/main
sub = repo:acme/backend-api:ref:refs/heads/main
workflow = deploy
Nothing was minted — this was a dry run.It exits non-zero when nothing matches, so it works as a CI assertion. The web app's Workload Identity page shows the same information from the other side: every recent exchange, which rule matched, and the claims it matched on.
Token lifetime
Exchanged tokens default to 15 minutes and are capped at 1 hour. Long-running commands re-exchange
transparently — a vaultic run --watch process outliving its token doesn't die, it quietly gets
another one, because the ambient OIDC token is still there.
Exchanged tokens are never listed on the Tokens page and can't be revoked individually. To stop a workload authenticating, delete its trust rule or disable the provider; anything already minted expires on its own within minutes.
Auditing
Each exchange writes a workload.exchanged audit entry carrying the matched rule and the
workload's subject claims, and fires a workload.authenticated
webhook event. So the audit log answers which workflow run read production,
rather than token sht_ab12… read production.
Environment variables
| Variable | Purpose |
|---|---|
VAULTIC_WORKLOAD_PROVIDER | Provider slug to exchange against. Enough on its own — no login step needed. |
VAULTIC_WORKLOAD_WORKSPACE | Only when addressing a provider by name instead of slug. |
VAULTIC_WORKLOAD_AUDIENCE | Audience to request; defaults to vaultic. |
VAULTIC_WORKLOAD_TOKEN | Supply the OIDC token directly, bypassing detection. |
VAULTIC_WORKLOAD_TOKEN_FILE | Read the OIDC token from a file (projected volumes). |