Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

CI/CD pipelines

Authenticate pipeline jobs with scoped API tokens, inject secrets into deploy commands, and plan for token expiry.

CLI v7.8.1

Your pipeline needs production secrets to deploy, but they should not live in the repository, sit in a dozen CI variables, or end up in a job log. With varsafe, the CI system holds exactly one credential: an API token scoped to that pipeline’s project and environments. The deploy command runs under varsafe run, which fetches the secrets and hands them to that process alone. Rotate a secret in varsafe and the next run picks it up, with no CI variable to update. Every fetch is recorded in the audit log under the token’s name.

Prerequisites

You need

A varsafe project holding the secrets the pipeline needs.

Role

The Owner or Admin role in its team, to create the token.

CI secret store

Access to your CI system's secret store: GitLab CI/CD variables or GitHub Actions secrets.

1. Create a token for the pipeline

  1. In the dashboard, open API tokens under Access, choose the team, and click Create API Token.
  2. Name it after the pipeline that will use it, for example github-actions-production-deploy. The name is what the audit log shows.
  3. Under What can this token do?, choose Read secrets. A deploy that only injects configuration never needs to write. See access level. A job that does change secrets in production, or another protected environment, needs Read and change secrets and that environment chosen under Scope: a token for all environments cannot change a protected environment.
  4. Under Scope, narrow it to the project, and the environments, this pipeline deploys.
  5. Pick an expiry. See Plan for token expiry for the allowed lifetimes.
  6. Copy the vs_at_... token. It is shown only once.

A token belongs to one team and cannot reach another team’s secrets. Create one per pipeline rather than sharing one: each can be revoked on its own, and the audit log tells you which pipeline did what. If a read-only token leaks, it exposes what that pipeline could read, but it cannot change or delete anything. An existing token can be narrowed to read-only later without rotating it, so check the tokens you already have.

2. Store the token in your CI secret store

Go to Settings → CI/CD → Variables and add a variable:

Key:   VARSAFE_API_TOKEN
Value: vs_at_...
Flags: Masked, Protected

Masked keeps the value out of job logs. Protected exposes it only to pipelines on protected branches and tags, so a merge request branch cannot read it.

Go to Settings → Secrets and variables → Actions and click New repository secret:

Name:  VARSAFE_API_TOKEN
Value: vs_at_...

GitHub masks registered secrets in log output, and does not pass them to workflows triggered from forks.

3. Authenticate the job

The CLI reads the token from VARSAFE_API_TOKEN in the job’s environment. VARSAFE_TOKEN is accepted as a fallback when VARSAFE_API_TOKEN is not set. There is no login step.

The variable from step 2 is already in the environment.

Map the secret into the step’s env:

env:
  VARSAFE_API_TOKEN: ${{ secrets.VARSAFE_API_TOKEN }}

A stored varsafe login credential takes precedence over VARSAFE_API_TOKEN. A fresh CI runner has no stored login, so it uses the token. On your developer machine, an existing login wins even when you set a pipeline token locally. Run varsafe whoami to check which credential the CLI uses.

To fail fast on a revoked, expired or mistyped token, put a cheap authenticated call at the top of the job:

varsafe whoami

4. Name the project by ID

A CI job cannot answer prompts, so give the project and environment explicitly. Use -i/--project-id rather than -p/--project: the ID stays the same when the project is renamed.

varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh

To find the ID, run varsafe use in your repository on your own machine. It writes the selected projectId into a .varsafe file at the repository root (IDs only, no secrets). Store the ID as a plain CI variable; it is not sensitive.

5. Wrap the deploy command with varsafe run

varsafe run fetches the secrets, injects them into the child process’s environment, and passes the command’s arguments through exactly as written. The values never touch the runner’s filesystem.

varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- docker compose up -d

Narrow what gets injected with --include glob patterns, comma-separated or repeated:

varsafe run -i "$VARSAFE_PROJECT_ID" -e production --include 'DB_*,API_*' -- ./deploy.sh

The token itself is not passed on: varsafe run removes VARSAFE_API_TOKEN and VARSAFE_TOKEN from the command’s environment, so the deploy script and the tools it starts do not inherit it. Code running as the same user on the runner can still find the token, so narrow its scope to what the job needs. Add --pass-credential only when the command itself has to call varsafe.

Plan for token expiry

Every token expires. Choose 1, 7, 30 or 90 days, or 365 on the Team plan. After expiry, authenticated pipeline commands fail with The API token in VARSAFE_API_TOKEN was rejected.

Set a calendar reminder to replace the token before it expires. Then:

  1. Create a new token with the same scope.
  2. Update the CI secret.
  3. Verify it with varsafe whoami.
  4. Revoke the old token.

Rotation replaces the token value and invalidates the old value immediately, but keeps the original expiry date. Rotating before expiry does not renew its lifetime, and an expired token cannot be rotated.

Staging and production from one pipeline

Create one token per environment: scope the staging token to the project and staging, and the production token to the project and production. Supply only that environment’s token as VARSAFE_API_TOKEN in each deploy job, and select the matching environment with -e. See token scope.

Complete examples

# Inject secrets into a GitLab CI job with an API token.
# VARSAFE_API_TOKEN is a masked, protected CI/CD variable; VARSAFE_PROJECT_ID a plain one.
deploy:
  image: oven/bun:1
  script:
    # The Bun image has no curl, which the installer is fetched with.
    - apt-get update && apt-get install -y --no-install-recommends curl
    - curl -fsSL https://varsafe.dev/install.sh | bash
    - export PATH="$HOME/.varsafe/bin:$PATH"
    # No login step: every command reads VARSAFE_API_TOKEN from the job environment.
    # Passing a token inline (`--token "$VARSAFE_API_TOKEN"`) is refused — argv leaks
    # to shell history and process listings.
    - varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- bun run deploy

The oven/bun image has no curl, so the job installs it before running the installer.

# Inject secrets into a GitHub Actions job with an API token.
# VARSAFE_API_TOKEN is a repository secret; VARSAFE_PROJECT_ID a repository variable.
name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install varsafe
        run: |
          curl -fsSL https://varsafe.dev/install.sh | bash
          echo "$HOME/.varsafe/bin" >> "$GITHUB_PATH"
      - name: Deploy with secrets
        env:
          VARSAFE_API_TOKEN: ${{ secrets.VARSAFE_API_TOKEN }}
        # No login step: every command reads VARSAFE_API_TOKEN from the job environment.
        # Passing a token inline (`--token "$VARSAFE_API_TOKEN"`) is refused — argv leaks
        # to shell history and process listings.
        run: |
          varsafe run -i "${{ vars.VARSAFE_PROJECT_ID }}" -e production -- npm run deploy

VARSAFE_PROJECT_ID is a repository variable (Settings → Secrets and variables → Actions → Variables), not a secret.

When the pipeline needs a file

Prefer varsafe run: most tools read configuration from the environment. When a tool insists on a file, such as docker compose --env-file, use this Linux-only recipe on a runner with /dev/shm mounted as tmpfs:

(
  env_file=/dev/shm/varsafe-$$.env
  trap 'rm -f "$env_file"' EXIT
  varsafe export -i "$VARSAFE_PROJECT_ID" -e production --plain --tmpfs -o "${env_file##*/}" &&
    docker compose --env-file "$env_file" up -d
)

The trap deletes the file when the subshell exits, whether the deploy succeeded or failed, and && stops Compose from running on a stale file if the export fails.

  • --tmpfs writes to /dev/shm/ plus the file name from -o, discarding any directory part. It creates and mounts nothing, so where /dev/shm does not exist, as on macOS, the export fails and && skips the next command. The file is 0600, readable by you only. The process ID in the name keeps two jobs running as the same user on one runner from reading or deleting each other’s file.
  • --plain is needed because the tool reads the file itself. varsafe export encrypts .env output by default, and only varsafe run --env-file can open the result, under a pipeline token as well as a login. Encrypt when the file is for varsafe run to read, now or later; use --plain when another tool in the same job has to read it. See encrypted .env.

On macOS, prefer varsafe run without a file. If a tool requires one, omit --tmpfs and use a private temporary directory:

(
  umask 077
  env_dir=$(mktemp -d "${TMPDIR:-/tmp}/varsafe.XXXXXX") || exit 1
  trap 'rm -rf "$env_dir"' EXIT
  varsafe export -i "$VARSAFE_PROJECT_ID" -e production --plain -o "$env_dir/app.env" &&
    docker compose --env-file "$env_dir/app.env" up -d
)

Without --tmpfs, export writes to the path you provide with mode 0600, including on macOS. This temporary file can reach disk; the trap removes its directory when the subshell exits.

Keep secrets out of job logs

  • Register the token as a masked variable, so the CI system redacts it if anything prints it.
  • Never echo injected variables, and keep varsafe list -r and varsafe get out of job scripts: their output lands in the log.
  • Be careful with set -x: shell tracing prints expanded command lines, including any secret passed as an argument.

Self-hosted or staging API

Set VARSAFE_API_URL to point the CLI at another API instance:

VARSAFE_API_URL=https://varsafe.internal.example.com varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh

Verify it works

  • varsafe whoami at the top of the job confirms the token is accepted, and shows what it may do, before the deploy runs.
  • The Audit log in the dashboard records every fetch the token makes, with the token named as the actor.

Troubleshooting

The job prints Why, and the fix
The API token in VARSAFE_API_TOKEN was rejected The API refused the token. It was revoked, rotated (rotation invalidates the old value at once), expired, or mistyped. For an expired or revoked token, create a replacement and update the CI variable. See The API token was rejected.
Project is required. Use -p <project> or -i <project-id> to specify. The job cannot prompt and no project was given. Add -i "$VARSAFE_PROJECT_ID", and -e for the environment. See Project is required.
varsafe: command not found The install step did not put ~/.varsafe/bin on PATH for this step. See the PATH lines in the examples above.

More failure modes: Troubleshooting.