seekrit
Docs/GitHub Actions

Sync to GitHub Actions

caution

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 writesActions secrets, at repository, deployment environment, or organization scope
Addressed byowner/repo, plus an environment name — or an organization login and a visibility
Connection carriesThe token, and a base URL for GitHub Enterprise Server only
Token permissionFine-grained Secrets repository permission (write), or the organization Secrets permission
Takes effectNext workflow run
Value visibilityWrite-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 the secrets context when the step is created.
  • secrets: inherit into a reusable workflow, which passes the calling workflow's secrets context wholesale.
  • Job-level container: and services: 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
caution

--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 production environment, or to its organization — the three are different recipients. So a run is one GET for the key, then one PUT per 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 GET costs 1 and a PUT or DELETE costs 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

note

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.

note

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

SymptomCauseFix
403 on every nameThe token lacks the Secrets permission for that scope, or the fine-grained token doesn't include this repositoryRepository, environment, and organization secrets are three permissions — check the one the binding writes
404 on the environmentThe deployment environment doesn't existCreate it in Settings → Environments. seekrit will not create one, because that would silently drop its protection rules
404 on the repository with a valid tokenA fine-grained token that does not list this repositoryAdd it to the token's repository access
A whole set of names failed as a collisionTwo mapped names differ only by caseGitHub is case-insensitive; rename or filter all but one
One name failed, rest landedThe mapped name starts with a digit, begins GITHUB_, or the value is over 48 KBFix the prefix or rename
422 on an org write--gh-visibility selected with no repository IDs, or IDs given for another visibilityPass numeric IDs with selected, and none otherwise
Rate-limited, run reported partialGitHub's secondary rate limit — mutating calls cost 5 points eachNothing to do — seekrit backs off and retries the rest
A rotated value isn't picked upActions read GitHub's copy at run startThe next workflow run has it. Or drop sync and use the Action, which needs no re-sync at all

See also