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 what apivault run and apivault env export need.
  • 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.

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.

.github/workflows/deploy.ymlShell
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 build

apivault 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.

DockerShell
docker run --rm \
  -e APIVAULT_TOKEN="$APIVAULT_TOKEN" \
  my-app:latest npx apivault run -- node server.js
KubernetesShell
env:
  - name: APIVAULT_TOKEN
    valueFrom:
      secretKeyRef:
        name: apivault
        key: token

Without 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.

List keysShell
curl https://apivault.tech/api/keys \
  -H "Authorization: Bearer $APIVAULT_TOKEN"
Export an .env for one environmentShell
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.

Two secrets, not oneShell
env:
  APIVAULT_TOKEN: ${{ secrets.APIVAULT_TOKEN }}
  APIVAULT_KEY: ${{ secrets.APIVAULT_KEY }}

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:

VerifyShell
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.

CommandAvailableRequires
keys list, keys getYeskeys:read
keys get --revealYeskeys:reveal
keys add (flags only), keys delete -fYeskeys:write
keys updateNoPrompts for every field; no flag equivalent
run, env exportYeskeys:read + keys:reveal
whoami, config, linkYesAny token
projects listNoSpans every project; a token belongs to one
logs, logs tail, logs getNoAudit surface — read it in the web app
login, logoutNoRefused 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.