Sync to GitHub Actions
Read this before setting one up: you probably want the Action instead.
GitHub Actions is a runtime seekrit can reach into, which makes it the one
destination here where a strictly better option already exists. The published
seekritdev/github-action
resolves and decrypts inside your workflow run:
- uses: seekritdev/github-action@v1
with:
token: ${{ secrets.SEEKRIT_TOKEN }}
app: storefront
env: production
Nothing is copied to GitHub, nothing is stored there, a rotated value is picked up on the next run with no re-sync, and seekrit's servers never decrypt anything. Syncing gives all three of those up.
Sync is the right answer only where the Action cannot reach — see when to sync anyway below.
| At a glance | |
|---|---|
| What seekrit writes | Actions secrets, at repository, deployment environment, or organization scope |
| Addressed by | owner/repo, plus an environment name — or an organization login and a visibility |
| Connection carries | The token, and a base URL for GitHub Enterprise Server only |
| Token permission | Fine-grained Secrets repository permission (write), or the organization Secrets permission |
| Takes effect | Next workflow run |
| Value visibility | Write-only — GitHub never discloses a secret value again |
When to sync anyway
GitHub resolves some things before any step of yours could run, so no Action can supply them. Those are the cases this connector is for:
- A third-party action that takes a credential as a
with:input — the value is interpolated from thesecretscontext when the step is created. secrets: inheritinto a reusable workflow, which passes the calling workflow'ssecretscontext wholesale.- Job-level
container:andservices:credentials, resolved before the job's first step. - Dependabot and Codespaces, which read their own secret stores and run no workflow you control. (Those are separate GitHub APIs; this connector writes Actions secrets only.)
If your situation is not one of these, use the Action.
1. Create a token
Prefer a fine-grained personal access token with the Secrets repository permission set to Read and write, scoped to only the repositories this connection syncs. For organization secrets, it needs the organization Secrets permission instead.
A classic token needs repo — or admin:org for organization secrets — which
grants vastly more than writing secrets. Use fine-grained where you can.
printf '%s' "$GITHUB_TOKEN" \
| seekrit sync connect --name acme-github --provider github-actions
A github.com connection has no second half to state: a GitHub token addresses
everything by owner/repo or org, and those are the binding's business. For a
self-hosted GitHub Enterprise Server appliance, add --base-url https://github.acme.com/api/v3. Leave it off for github.com and for Enterprise
Cloud. It must be https — that URL carries the token.
2. Bind an environment
GitHub has three secret scopes, and a binding picks one. They are three different endpoints with three different blast radii — and each has its own encryption key, so a value sealed for one cannot be written to another.
A repository's secrets
Readable by every workflow in it, including one added by a pull request from a collaborator with write access. That is GitHub's model, not seekrit's — and it is the reason to prefer the environment scope for anything touching production.
seekrit sync verify acme-github --provider github-actions \
--gh-repo acme/storefront
seekrit sync enable --connection acme-github --provider github-actions \
--gh-repo acme/storefront \
--app storefront --env production --acknowledge-decryption
--gh-repo takes the pair as one flag, because that is how GitHub writes a
repository everywhere; asking for it in two invites pasting the pair into one of
them.
A deployment environment's secrets
The narrowest scope GitHub has, and the one to prefer:
seekrit sync enable --connection acme-github --provider github-actions \
--gh-repo acme/storefront --gh-environment production \
--app storefront --env production --acknowledge-decryption
A job reads these only by declaring environment: production, which also
subjects it to that environment's protection rules — required reviewers, wait
timers, and the branch policy. That combination is as close as GitHub gets to
"this secret is for production, and reaching it takes an approval".
The environment must already exist. seekrit will not create one: an environment is a deployment gate, and silently creating an unprotected one because a name was misspelled would quietly remove the protection you were relying on.
An organization's secrets
The widest scope in the product, and the only destination on any provider that can hand a value to repositories nobody named.
seekrit sync enable --connection acme-github --provider github-actions \
--gh-org acme --gh-visibility selected --gh-repo-ids 1296269,1296270 \
--app storefront --env production --acknowledge-decryption
--gh-visibility all hands the value to every repository in the
organization — including ones added tomorrow, and public ones. seekrit
deliberately has no default here and defaults the CLI to private; state what
you mean.
selected takes numeric repository IDs, not names, because that is what
GitHub's API takes. Get one with gh api repos/acme/storefront --jq .id.
seekrit does not resolve names to IDs on your behalf: guessing which repository
an ambiguous name meant would widen a secret's reach silently. Passing IDs with
any other visibility is refused rather than ignored, so a stored binding never
contradicts its own behavior.
How a push behaves
-
The value is encrypted before it leaves seekrit. GitHub does not accept a plaintext secret: each value is sealed to the scope's own X25519 public key as a libsodium sealed box, and GitHub decrypts it on receipt.
One nice consequence: GitHub never sees a plaintext value at all, so it cannot echo one back into an error message — the failure reasons stored on a run row cannot carry a secret value even in principle. Other connectors scrub responses for exactly that reason; here there is nothing to scrub.
-
Each scope has its own key, fetched once per run. A value sealed for a repository cannot be written to that repository's
productionenvironment, or to its organization — the three are different recipients. So a run is oneGETfor the key, then onePUTper name. -
A run is capped at 150 operations, sized against GitHub's secondary rate limit rather than its primary one: 900 points a minute, where a
GETcosts 1 and aPUTorDELETEcosts 5. That is 180 mutating requests a minute, well below the 5,000-an-hour primary limit. -
seekrit cannot detect value drift here, only presence. Actions secrets are write-only: once set, no one can read a value back through the UI, CLI, or API — which is what you want for something a secrets manager owns.
Values take effect on the next workflow run; a sync run starts nothing.
Name and value rules
Secret names are case-insensitive at GitHub, which no other destination
here is. db_url and DB_URL are one secret, so a name mapping that produces
both would write it twice and report both as pushed while one value silently
won.
seekrit detects that before it writes anything and fails every name in the collision, naming them — it cannot know which one you meant. Watch for it if the binding folds case. Rename or filter all but one.
GitHub will not accept every name seekrit will. Secret names take only letters,
numbers, and underscores, must start with a letter or an underscore, and cannot
begin with GITHUB_, which GitHub reserves for itself. Values stop at 48
KB, checked against the plaintext — the number you can act on — and before
sealing, so an over-size body is never built to be refused. A name that breaks
one of those is reported as a failure against that name alone; the rest of the
environment still pushes. Watch for it if you use a --prefix.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
403 on every name | The token lacks the Secrets permission for that scope, or the fine-grained token doesn't include this repository | Repository, environment, and organization secrets are three permissions — check the one the binding writes |
404 on the environment | The deployment environment doesn't exist | Create it in Settings → Environments. seekrit will not create one, because that would silently drop its protection rules |
404 on the repository with a valid token | A fine-grained token that does not list this repository | Add it to the token's repository access |
| A whole set of names failed as a collision | Two mapped names differ only by case | GitHub is case-insensitive; rename or filter all but one |
| One name failed, rest landed | The mapped name starts with a digit, begins GITHUB_, or the value is over 48 KB | Fix the prefix or rename |
422 on an org write | --gh-visibility selected with no repository IDs, or IDs given for another visibility | Pass numeric IDs with selected, and none otherwise |
| Rate-limited, run reported partial | GitHub's secondary rate limit — mutating calls cost 5 points each | Nothing to do — seekrit backs off and retries the rest |
| A rotated value isn't picked up | Actions read GitHub's copy at run start | The next workflow run has it. Or drop sync and use the Action, which needs no re-sync at all |
See also
seekritdev/github-action— the option to prefer- CI/CD & containers — wiring seekrit into pipelines without a copy at the destination
- Third-party sync — the shared model, naming and filtering, deletions