CI/CD Pipelines
Authenticate pipelines with a team-scoped API token and inject secrets into deploy commands without writing .env files or leaking values into job logs.
By the end of this page your pipeline authenticates with a team-scoped API token and injects secrets directly into your deploy command’s process environment — no .env file in the repository, no secrets on the runner’s disk, nothing echoed to the job log.
Prerequisites
- A varsafe project containing the secrets your pipeline needs
- A team role that can create API tokens
- Access to your CI system’s secret store (GitLab CI/CD variables or GitHub Actions secrets)
1. Create a CI token
- In the dashboard, open your team settings and go to API Tokens
- Click Create API Token and name it after the pipeline that will use it (e.g.
github-actions-production-deploy) - Under Access, choose Read-only unless the pipeline genuinely writes secrets — a deploy job that only injects configuration never needs write. See access level
- Copy the
vs_at_...token immediately — it is shown only once
API tokens are scoped to the team they were created in: a leaked token can never reach another team’s secrets. For least privilege, create one token per pipeline rather than sharing a single token — token names appear in the audit log, so per-pipeline tokens let you trace every action back to the job that performed it.
A read-only token narrows the blast radius further: if it leaks, an attacker can read what that pipeline could read, but cannot rotate or delete your secrets. An existing token can be changed to read-only later without a rotation window, so this is worth checking on 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_TOKEN
Value: vs_at_...
Flags: Masked, ProtectedMasked keeps the value out of job logs; Protected restricts it to protected branches and tags, so a merge request from a fork cannot exfiltrate it.
Go to Settings → Secrets and variables → Actions and click New repository secret:
Name: VARSAFE_API_TOKEN
Value: vs_at_...GitHub Actions automatically masks registered secrets in log output.
3. Authenticate in the job
Set VARSAFE_API_TOKEN (or the shorter alias VARSAFE_TOKEN) in the job environment. Every varsafe command picks it up automatically. There is no login step:
export VARSAFE_TOKEN="$CI_VARSAFE_TOKEN"
varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh
Because there is no login step, an invalid token first surfaces on the command that needs it. To fail fast on a revoked or mistyped token, put a cheap authenticated call at the top of the job:
varsafe whoami
4. Pin the project with --project-id
CI jobs are non-interactive: when the CLI cannot prompt, it needs the project and environment stated explicitly. Use -i/--project-id rather than -p/--project — the ID is a stable UUID that survives project renames:
varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh
To find the ID, run varsafe use locally inside your repository — it writes the selected projectId into a .varsafe file at the repo 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 secrets, injects them into the child process environment, and passes the command’s arguments through exactly as written. The secrets exist only in that process — they 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:
varsafe run -i "$VARSAFE_PROJECT_ID" -e production --include 'DB_*,API_*' -- ./deploy.sh
Complete examples
GitLab CI
# Inject secrets into a GitLab CI job with an API token.
# VARSAFE_TOKEN is a masked, protected CI/CD variable.
deploy:
image: oven/bun:1
script:
- curl -fsSL https://varsafe.dev/install.sh | bash
- export PATH="$HOME/.varsafe/bin:$PATH"
# No login step: every command reads VARSAFE_TOKEN from the job environment.
# Passing a token inline (`--token "$VARSAFE_TOKEN"`) is refused — argv leaks
# to shell history and process listings.
- varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- bun run deployGitHub Actions
# Inject secrets into a GitHub Actions job with an API token.
# VARSAFE_API_TOKEN is a repository secret.
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_TOKEN: ${{ secrets.VARSAFE_API_TOKEN }}
# No login step: every command reads VARSAFE_TOKEN from the job environment.
# Passing a token inline (`--token "$VARSAFE_TOKEN"`) is refused — argv leaks
# to shell history and process listings.
run: |
varsafe run -i "${{ vars.VARSAFE_PROJECT_ID }}" -e production -- npm run deployWhen the pipeline needs a file
Prefer varsafe run — most tools read configuration from the environment. When a consumer genuinely requires a file (e.g. docker compose --env-file), export one, keep it in RAM, and delete it in the same command chain:
varsafe export -i "$VARSAFE_PROJECT_ID" -e production --plain --tmpfs -o app.env
docker compose --env-file /dev/shm/app.env up -d
rm -f /dev/shm/app.env
--tmpfswrites to/dev/shm/(RAM-backed), so the plaintext never reaches persistent disk--plainis needed here becausedocker compose --env-filereads the file itself.varsafe exportencrypts.envby default and that works fine under a token — but nothing in CI can decrypt it, since the private key is restricted to an interactive Owner/Admin session. Encrypt when the artifact is for a human to use later; use--plainwhen a tool in the same job has to read it. See encrypted .env.
Keep secrets out of job logs
- Register the token as a masked variable so the CI system redacts it if a command prints it
- Never
echoinjected variables, and avoidvarsafe list -rorvarsafe getin job scripts — their output lands in the log - Be careful with
set -x(shell tracing): it 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 a different API instance — it takes priority over the URL saved at login:
VARSAFE_API_URL=https://varsafe.internal.example.com varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh
Verify it works
- Add
varsafe whoamiafter the auth step — it confirms the token is valid before the deploy runs - Check the Audit Log in the dashboard: every fetch performed with the token is recorded, with the token identified as the actor
Troubleshooting
Invalid or expired token— the token was revoked or rotated, or a stale saved context points at a team the token cannot access. See Invalid or expired token.Project is required. Use -p <project> or -i <project-id> to specify.— the job is non-interactive and no project was given. Add-i "$VARSAFE_PROJECT_ID"(and-efor the environment) to the command.- More failure modes: Troubleshooting.