CI/CD & service tokens
A service token is a machine credential for pipelines, containers and scheduled jobs — anywhere a browser sign-in is impossible. It belongs to a project rather than to a person, carries its own scopes, and always expires.
When to use one
apivault login opens a browser and writes a device token to ~/.apivault/token.json. A GitHub Actions runner has neither, so a service token is handed to the CLI through an environment variable instead.
The difference that matters is ownership. A device token is you: it can reach every project you belong to, and it dies when you leave. A service token belongs to one project, can do only what its scopes allow, and keeps working after the engineer who created it moves on — so a deploy does not break because somebody changed teams.
Creating a token
Open your project and go to Service Tokens, then New token. Only project owners and admins can create, view or revoke them.
Scopes
Three permissions, granted independently. Give a token the least it needs — a pipeline that only reads secrets should not be able to delete them.
keys:read— list key names, services, environments and masked values. Always granted; a token that cannot list cannot be used.keys:reveal— decrypt and return full secret values. This is whatapivault runandapivault env exportneed.keys:write— create, update and delete keys. Most pipelines do not need this.
A token that lists key names for a drift check is a far smaller liability than one that decrypts them, which is why those are separate grants rather than a single “read”.
Environment pin
A token can be pinned to a single environment. A staging pipeline's token then cannot read Production secrets no matter what it asks for — the pin replaces the environment in every request rather than acting as a default the caller can override. Asking for a different one explicitly returns 403 ENVIRONMENT_MISMATCH, and a key outside the pin returns 404 rather than confirming it exists.
IP allowlist
Optionally restrict a token to a list of addresses or CIDR ranges, IPv4 or IPv6. A request from anywhere else is refused, and so is one whose source address cannot be determined at all.
Addresses are compared in the forms proxies actually write, so an entry of 203.0.113.5 also matches ::ffff:203.0.113.5 and 203.0.113.5:443 — some platforms append the source port to every x-forwarded-for entry. Write allowlist entries as plain addresses or CIDR ranges and let the comparison handle the rest.
x-forwarded-for header. Behind a proxy that overwrites it — Vercel, Cloudflare, most managed platforms — that is trustworthy. On a self-hosted server reached directly, a caller can forge the header, so treat the allowlist as defence in depth rather than a hard boundary. Hosted runners also use large, changing address pools, so an allowlist suits a fixed build server better than GitHub Actions.If you put your own reverse proxy in front, it must set
x-forwarded-for. Setting only x-real-ip is not enough: a Node server fills in x-forwarded-for from the connection it can see, which behind a proxy is the proxy itself. Every request would then appear to come from one address and an allowlist would refuse them all.Expiry
Every token expires — 7, 30, 90, 180 or 365 days, defaulting to 90. There is deliberately no “never” option, so a forgotten pipeline credential cannot outlive the pipeline. The Service Tokens page shows an amber badge inside the last seven days and a red one once a token has expired.
GitHub Actions
Store the token as a repository secret, then hand it to the CLI as APIVAULT_TOKEN. No other configuration is needed — the token already knows its project.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Build with secrets
env:
APIVAULT_TOKEN: ${{ secrets.APIVAULT_TOKEN }}
run: npx apivault run -- npm run buildapivault run fetches the project's secrets, injects them as environment variables for the child process only, and never writes them to disk.
There is no --env flag in that step. A token pinned to an environment supplies it, so the runner needs no configuration of its own. Pass --env only for an unpinned token — on a pinned one it is redundant, and naming a different environment is refused rather than quietly served from the wrong place.
Docker and Kubernetes
Pass the token like any other secret. Prefer a mounted secret or your orchestrator's secret store over baking it into an image.
docker run --rm \ -e APIVAULT_TOKEN="$APIVAULT_TOKEN" \ my-app:latest npx apivault run -- node server.js
env:
- name: APIVAULT_TOKEN
valueFrom:
secretKeyRef:
name: apivault
key: tokenWithout the CLI
A service token is an ordinary bearer token against the REST API, so anything that can make an HTTP request can use one.
curl https://apivault.tech/api/keys \ -H "Authorization: Bearer $APIVAULT_TOKEN"
curl "https://apivault.tech/api/keys/export?environment=Production" \ -H "Authorization: Bearer $APIVAULT_TOKEN"
Service tokens reach the key endpoints only. Project settings, members, invitations, encryption configuration and token creation itself all reject them, so a leaked CI credential cannot add a collaborator, rotate your vault key, or mint a replacement for itself.
Projects with a custom vault key
If your project uses custom encryption mode, the server holds no recoverable copy of your vault key — so CI has to supply it alongside the token.
env:
APIVAULT_TOKEN: ${{ secrets.APIVAULT_TOKEN }}
APIVAULT_KEY: ${{ secrets.APIVAULT_KEY }}APIVAULT_TOKEN.Auditing and revoking
Every request a service token makes is recorded in the project's audit log under its own service source, tagged with the token's name. Refusals are recorded too — an expired token, a call outside its scope, or a request from an address that is not on its allowlist all leave a visible line rather than a silent 401.
Exporting an entire environment is the highest-risk thing a token can do, so when one does it the project's owners and admins are notified, with the token named.
Revoking takes effect immediately from the Service Tokens page. The record of what the token did is kept.
Checking which credential you picked up
APIVAULT_TOKEN takes precedence over any stored device token, which is usually what you want but occasionally surprising on a developer machine. Ask the CLI which identity it is actually using:
apivault whoami
With a service token this prints the token's name, project, scopes and environment pin instead of an email address. apivault login and apivault logout refuse to run while APIVAULT_TOKEN is set, so you cannot accidentally sign a device in or out from inside a pipeline.
Which CLI commands work
A service token reaches keys and nothing else. Commands that span the whole account, or that exist for human oversight, are not available to one — they refuse up front and say why rather than returning a bare authentication error.
| Command | Available | Requires |
|---|---|---|
keys list, keys get | Yes | keys:read |
keys get --reveal | Yes | keys:reveal |
keys add (flags only), keys delete -f | Yes | keys:write |
keys update | No | Prompts for every field; no flag equivalent |
run, env export | Yes | keys:read + keys:reveal |
whoami, config, link | Yes | Any token |
projects list | No | Spans every project; a token belongs to one |
logs, logs tail, logs get | No | Audit surface — read it in the web app |
login, logout | No | Refused while APIVAULT_TOKEN is set |
With no terminal to prompt on, keys add takes every value from flags: --name and --key are required, --environment falls back to the token's pin and is required when the token is unpinned, and --service defaults to Custom. A missing flag is named as an error rather than hanging the step waiting for input that will never arrive.
keys delete needs -f. Deleting cannot be undone, so with no terminal to confirm on the CLI refuses rather than reading an absent human as agreement. On a project with a custom vault key, pass --vault-key or set APIVAULT_KEY — any command that decrypts will otherwise stop for lack of a prompt.
A token that is expired, revoked, or blocked by its IP allowlist says so exactly. A bare APIVAULT_TOKEN was not recognised means the value never matched a token at all — usually a truncated copy or a secret set on the wrong repository, not an expiry.