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 createin 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 theest_…value shown once after Create into GitHub Secrets, and rundotvault pull demo-app --env productionin the workflow. Naming the project means the repository does not need aproject-config.json. - What the token reaches. Read only blocks writes, and a project name other than
demo-appis 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.
pullandrunfail with✖ AUTH_REQUIRED: invalid service token(exit 3); the raw API returns HTTP 401.dotvault whoamionly reads the token it has, so check a token withpull, notwhoami.
Before you read on
- Where we are. This continues with the
demo-appproject from part 2. In part 6, two Global Keys were bound to it (CF_API_TOKENandSENTRY_DSN_WEB). Before these captures I setDATABASE_URLandSTRIPE_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_TOKENin 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 thenpm installwarnings are dropped and shown as a single "…" line. Exit codes recorded as [exit=N] in the raw output appear asecho $?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-…andcf_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.
Create token opens a dialog with four fields: Name, Role, Project, Expiry. The defaults are Read only, All projects (whole org), and 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.
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.
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.
After Done, the list has a new row. For ci-deploy, Expires is 90 days after Created.
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
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.
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
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
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>.
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.
Right after revoking, I re-ran the run from section 5 (gh run rerun, the same as Re-run jobs in GitHub).
The job stopped at the pull with exit code 3, and the build step never ran. The terminal agrees:
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.












