Giving CI your own login? Pull secrets into GitHub Actions with one service token

2026-11-06来源 dev.toEnglish
Giving CI your own login? Pull secrets into GitHub Actions with one service token

This is part 7 of the series "dotvault, Start to Finish: One Command from Your Laptop to CI, Your Team, and Your AI Agents." dotvault will be released free of charge when this series ends.

GitHub Secrets can end up holding every line of your .env, or a personal login that lets CI act as you. Neither is needed. With dotvault, GitHub holds exactly one secret, a service token, and the workflow pulls the values at run time with dotvault pull demo-app --env production. In my test, the Actions log showed 5 keys written to .env and the build reporting DATABASE_URL, STRIPE_SECRET_KEY, and API_BASE_URL as set. When I revoked the token in the console and re-ran the same workflow, it stopped at the pull step with AUTH_REQUIRED and exit code 3, before the build ran.

This is part 7 of 12 in a series on dotvault, which keeps .env secrets for a team and hands them to people, CI and AI agents. Everything below was run against the production console and CLI 0.15.7, with GitHub Actions runs in a private repository.

The short version

  • Service tokens are issued in the console only. Service Tokens → Create token, then Name, Role, Project and Expiry. dotvault token create in the CLI just points you to the console. For CI, pick Read only, the one project, and an expiry (the default is Never).
  • CI holds one secret: DOTVAULT_TOKEN. Copy the est_… value shown once after Create into GitHub Secrets, and run dotvault pull demo-app --env production in the workflow. Naming the project means the repository does not need a project-config.json.
  • What the token reaches. Read only blocks writes, and a project name other than demo-app is refused (tested with a name that does not exist). The org's Global Keys pool is off limits too. Tokens are scoped by project, so the same token can pull that project's development and staging as well.
  • Revoke stops the next run. pull and run fail with ✖ AUTH_REQUIRED: invalid service token (exit 3); the raw API returns HTTP 401. dotvault whoami only reads the token it has, so check a token with pull, not whoami.

Before you read on

  • Where we are. This continues with the demo-app project from part 2. In part 6, two Global Keys were bound to it (CF_API_TOKEN and SENTRY_DSN_WEB). Before these captures I set DATABASE_URL and STRIPE_SECRET_KEY (dummy values) in production.
  • Setup. The production app.dotvault.io and CLI 0.15.7. The token runs used a Node 20 Linux container with no login and no OS keychain, only DOTVAULT_TOKEN in the environment, the same conditions as CI. GitHub Actions ran in a private repository made for this part.
  • Terminal and Actions logs are rebuilt. The text is the raw output (Actions logs via gh run view --log), laid out for reading. Timestamps and the npm install warnings are dropped and shown as a single "…" line. Exit codes recorded as [exit=N] in the raw output appear as echo $? lines.
  • Tokens are masked as est_•••• in screenshots and terminals. Both tokens created for this part were revoked afterwards.
  • Demo account. Project names, values and the org id are demo data made for this series. dummy-… and cf_demo_… are placeholders that work nowhere.
  • Terms. A service token is the credential CI or a server uses instead of a person's login (it starts with est_). Revoke disables it. A step is one stage of a workflow. An exit code is the number a command returns when it finishes: 0 is success.

1. The Service Tokens page and Create token

With no tokens yet, Service Tokens in the sidebar looks like this.

Service Tokens with no tokens: the notice "Non-interactive IDs for CI / Lambda / SDKs. The secret is shown only once, at issuance. If one leaks, revoke it immediately. Everything is recorded in Audit.", the Create token button, "Use a token by exporting DOTVAULT_TOKEN=est_… for the CLI/SDK.", and "No service tokens yet."

Create token opens a dialog with four fields: Name, Role, Project, Expiry. The defaults are Read only, All projects (whole org), and Never.

The Create service token dialog with its defaults: Name empty with ci-deploy as placeholder, Role Read only (viewer), Project All projects (whole org), Expiry Never.

There are three roles. The name in parentheses is dotvault's internal role name.

Label in the console Description in the console For CI?
Read only (viewer) Can pull secrets only. Minimal privilege for reading in CI. Yes (this part)
Read/write (member) In addition to pull, can add, update, and delete keys. Only if CI writes values
Admin / automation (provisioner) Can also perform admin operations such as creating projects and changing settings. Intended for Claude Code headless onboarding. No (part 8)

Choosing Admin / automation adds a warning about the impact of a leak and a note that its scope is the whole org whatever project you pick.

The dialog with Admin / automation (provisioner): the warning "provisioner is a strong credential … Because the impact of a leak is large, be careful who it is issued to and how it is stored." and, under Project, "provisioner only performs admin operations such as creating projects, so its scope is the whole org regardless of this choice".

Expiry offers Never, 30 days, 90 days and 1 year. Leaving the default creates a token that never expires. The console's own Help page says to keep the TTL short, so pick one for CI.

2. Creating the token

For CI: Name ci-deploy, Role Read only, Project demo-app, Expiry 90 days.

The dialog just before creating ci-deploy: Name ci-deploy, Role Read only (viewer), Project demo-app, Expiry 90 days.

Create shows the est_… token once. Copy it and put it in GitHub Secrets right away (section 5). Once this dialog closes, the token cannot be shown again. The image below is the same dialog for a second token (ci-nightly), with the token text masked before the capture.

Token created, token masked: "Copy this token now — it is shown only once and cannot be retrieved again.", est_••••, Copy and Done.

After Done, the list has a new row. For ci-deploy, Expires is 90 days after Created.

Service Tokens with two active rows: ci-nightly (Read only (viewer), demo-app, expiring 30 days after creation) and ci-deploy (Read only (viewer), demo-app, expiring 90 days after creation), each with Revoke.

The idea itself (shown once, revoke on a leak) was introduced in an earlier post about building dotvault. This part runs it end to end.

3. The CLI cannot issue tokens

dotvault token create --name ci-deploy (rebuilt): " raw `dotvault token create` endraw is no longer available in the CLI.", "Service tokens are created, listed, and revoked in the dotvault admin console:", "https://app.dotvault.io/admin/tokens", "The token API only accepts admin-console sessions, so a CLI login cannot manage tokens.", exit code 1.

token list prints the same message, with token list as the command name in the first line, and exits 1. Creating, listing and revoking all happen on the console's Service Tokens page.

4. Pulling with nothing but the token

Before involving GitHub, I checked the token on a terminal with no login, only DOTVAULT_TOKEN set and demo-app's project-config.json in the directory.

Token-only terminal (rebuilt): whoami prints "Service token (DOTVAULT_TOKEN=est_••••••••…) — CI / Lambda mode"; pull -E production prints "✓ Wrote 5 keys to /work/ci-run/.env (project=demo-app, env=production, file=.env)." and "✔ 1 file synced, 1 excluded, 5 keys total"; cat .env shows CF_API_TOKEN, SENTRY_DSN_WEB, API_BASE_URL, DATABASE_URL and STRIPE_SECRET_KEY.

whoami reports a service token instead of an email address. Of the 5 keys, DATABASE_URL and STRIPE_SECRET_KEY are production keys of the project, API_BASE_URL comes from its Common (_shared) layer, and the first two are the Global Keys bound in part 6. "1 excluded" is .env.local, which is declared in project-config.json but has no production values. dotvault run -E production -- <command> worked with the same token too, passing values to the command without writing a file.

5. Handing it to GitHub Actions

The secret

In the repository: Settings → Secrets and variables → Actions → New repository secret, named DOTVAULT_TOKEN, with the token as the value. I did the same with the GitHub CLI:

gh secret set DOTVAULT_TOKEN -R <owner>/<repo> # paste the token when prompted 

That is the only secret GitHub holds. DATABASE_URL and the rest stay in dotvault.

The workflow

# .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Pull dotvault secrets env: DOTVAULT_TOKEN: ${{ secrets.DOTVAULT_TOKEN }} run: | npm install -g @earthlink/dotvault dotvault whoami dotvault pull demo-app --env production cut -d= -f1 .env - name: Your build step run: npm run build 

Naming the project in dotvault pull demo-app --env production means no project-config.json in the repository. cut -d= -f1 .env prints the key names and dotvault's marker lines (# BEGIN and # END) only, so no values reach the log. The npm run build here is a small stand-in that reads .env and prints set or MISSING for DATABASE_URL, STRIPE_SECRET_KEY and API_BASE_URL, never the values. pull only writes .env; it does not turn the values into environment variables for the next step. This build reads .env itself, so the values arrived. A build that does not read .env gets nothing, so give such a build a line that checks the keys it needs.

The run

GitHub Actions log (rebuilt). Pull dotvault secrets: whoami "Service token (DOTVAULT_TOKEN=est_••••••••…) — CI / Lambda mode", pull "✓ Wrote 5 keys to /home/runner/work/dotvault-ci-demo/dotvault-ci-demo/.env (project=demo-app, env=production, file=.env)." and "✔ 1 file synced, 0 excluded, 5 keys total", the marker lines and five key names. Your build step: DATABASE_URL: set, STRIPE_SECRET_KEY: set, API_BASE_URL: set, build finished.

DOTVAULT_TOKEN: *** is GitHub masking the secret. The first 8 characters after est_ printed by whoami are not masked by GitHub and stay in the log (masked in the image). It says 0 excluded here because, with no project-config.json, only .env is written.

6. What this token reaches, and what it does not

What the token reaches (rebuilt): set FOO=1 -E production fails with "✖ PERMISSION_DENIED: … (server: service token is read-only (viewer); write requires a member token)." and 4; pull nonexistent-proj -E production fails with "(server: service token is not scoped to this project)" and 4; global status fails with "(server: no access to the org global key pool)"; pull -E development succeeds with 8 keys total and pull -E staging with 5 keys total.

Action with ci-deploy Result Exit code
set FOO=1 -E production PERMISSION_DENIED (service token is read-only (viewer); write requires a member token) 4
delete STRIPE_SECRET_KEY -E production the same PERMISSION_DENIED 4
pull nonexistent-proj -E production PERMISSION_DENIED (service token is not scoped to this project) 4
global status (the Global Keys pool) PERMISSION_DENIED (no access to the org global key pool) 4
pull -E production 5 keys 0
pull -E development 8 keys (.env 7 + .env.local 1) 0
pull -E staging 5 keys 0

Writes and other projects are refused. This org has no project besides demo-app, so "other project" was tested with a name that does not exist; the server's reason was that the token is not scoped to it. The Global Keys pool is not visible to this token either.

Tokens are scoped by project. The same token also pulled development (8 keys) and staging (5 keys). Issue a CI token for a project whose values that CI is allowed to receive.

7. Without the CLI: curl

Where the CLI cannot be installed, call the API with the token in the HTTP Authorization header as Bearer <token>.

curl (rebuilt): GET https://api.dotvault.io/v1/env/pull?project=demo-app&env=production&file=.env with Authorization: Bearer $DOTVAULT_TOKEN returns JSON with project, env, file, keys (five keys with values) and source (shared: API_BASE_URL; envSpecific: DATABASE_URL, STRIPE_SECRET_KEY; global: CF_API_TOKEN, SENTRY_DSN_WEB). Without a token: {"error":"UNAUTHORIZED","message":"Missing token"} and HTTP 401.

source says where each key came from, and global lists CF_API_TOKEN and SENTRY_DSN_WEB, confirming from the API side that the first two keys in section 4 are the Global Keys bound in part 6. Without file, the same object comes back inside a files array. Success is HTTP 200.

8. Revoking

Revoke on a row opens the browser's own confirmation, so there is no screenshot:

Revoke token “ci-deploy”? CI jobs and services using it will lose access immediately.

After OK, Status becomes revoked. The list filter has Active only (the default), All (incl. revoked) and Revoked only.

Service Tokens filtered to Revoked only: one row, ci-deploy (Read only (viewer), demo-app, revoked), with no Revoke button.

Right after revoking, I re-ran the run from section 5 (gh run rerun, the same as Re-run jobs in GitHub).

GitHub Actions re-run log (rebuilt, after Revoke). Pull dotvault secrets is marked failed: whoami still prints "Service token (DOTVAULT_TOKEN=est_••••••••…) — CI / Lambda mode", pull prints "✖ AUTH_REQUIRED: invalid service token", then "Error: Process completed with exit code 3." Your build step does not run.

The job stopped at the pull with exit code 3, and the build step never ran. The terminal agrees:

After Revoke (rebuilt): dotvault whoami still prints "Service token (…) — CI / Lambda mode" with 0; pull -E production and run -E production both fail with "✖ AUTH_REQUIRED: invalid service token" and 3; curl returns {"error":"UNAUTHORIZED","message":"invalid service token"} and HTTP 401.

whoami still reports "CI / Lambda mode" and exits 0 after revocation, because it only reads the token it has and does not ask the server. Check whether a token still works with pull. The error after revocation is AUTH_REQUIRED, not PERMISSION_DENIED; a made-up est_ value gave the same AUTH_REQUIRED: invalid service token (exit 3), so when a CI failure shows it, suspect a revoked token or a wrong secret value. With the secret empty (a misspelled secret name, for example), the error was "AUTH_REQUIRED: Not logged in. Run dotvault login first." (exit 3).

9. When it fails

Action Output Exit code
dotvault token create / token list dotvault token create / dotvault token list is no longer available in the CLI. … points to the console 1
set / delete with a Read only token PERMISSION_DENIED (read-only (viewer); write requires a member token) 4
pull of a name other than demo-app (tested with a nonexistent name) PERMISSION_DENIED (not scoped to this project) 4
pull / run after Revoke, or with a made-up est_ value AUTH_REQUIRED: invalid service token 3
pull with DOTVAULT_TOKEN empty (e.g. a misspelled secret name) AUTH_REQUIRED: Not logged in. Run dotvault login first. 3
whoami after Revoke Service token (…) — CI / Lambda mode (it only reads the local token) 0
API call with no token {"error":"UNAUTHORIZED","message":"Missing token"} HTTP 401

pull only writes .env, so a build that does not read .env, or that looks for a misspelled key name, can carry on without its values. One line in the job that shows which keys arrived catches that early (the cut -d= -f1 .env in section 5, or a set / MISSING check like this part's build).

Next

Part 8 hands tokens to an AI agent. It issues two new tokens, a Read/write token that can write values and the Admin / automation token whose warning appeared in section 1, hands them to an AI agent, and goes from creating a project to pulling it with no human login.

dotvault stores the secrets in your .env and config files server-side and hands them to any authorised developer, CI job or AI agent with a single command (https://dotvault.io). It will be released free of charge when this series ends, and the steps in these articles are the steps you will use on day one.

If you find this useful, please like and follow.

Our products are listed at https://www.eln.ne.jp/products

EarthLink Network builds its own products to run the whole company on AI — each one born from a need along the way. See the full index of what we build:

→ The 18 products EarthLink Network builds in-house

For more about the company and each product, visit www.eln.ne.jp.

原文:https://www.a8q9.cn/en/article/giving-ci-your-own-login-pull-secrets-into-github-actions-with-one-service-token-8d9e4e.html

相关文章