Secrets and Credentials#
Problem#
Workflows need secrets — registry tokens to push container images,
forge tokens for API access, user-defined API keys for deployment.
Today there is no mechanism to inject secrets into the build
environment beyond server-level environment variables
(MYCI_FORGE_TOKEN).
This blocks real-world use cases:
Pushing a Nix-built container image to a forge registry from a workflow step.
Deploying to external services that require authentication.
Any workflow that needs credentials the operator has not pre-configured as a server-level env var.
Current State#
MYCI_FORGE_TOKENis a single server-level token, configured in the k8s deployment secret. Used for commit status reporting.No workflow-level
secretscontext exists.No secret store exists.
Note
Secret masking is planned in sprint 19. Per-user forge tokens are planned in sprint 21.
Three Layers#
Secrets support builds incrementally in three layers. Each is independently useful.
Layer 1: Secret masking#
Prevent known secret values from appearing in log output, JSONL event files, and SSE streams.
A
SecretMaskersits between thempscreceiver and the JSONL writer. It replaces known values with***before events touch disk or broadcast.The
::add-mask::workflow command lets steps register additional values for masking at runtime.Initial masking list: forge token, OAuth credentials, any value from a future
secretscontext.File permissions on the data directory:
0700for directories,0600for files — consistent with the existing token store.
Masking is string replacement of known values. It cannot prevent encoding, splitting, or exfiltration of secrets via creative workflows. This is a fundamental limit shared by all CI systems.
Event envelope safety: tokens and secrets must never appear in
event envelope fields (RunStarted, JobStarted, etc.) — only
in Log event content where masking applies. Envelopes carry
metadata (repo, ref, SHA, job/step names) which is non-sensitive.
Layer 2: Per-user forge tokens#
The server uses the authenticated user’s forge token instead of a single global token. This enables:
Commit status reporting under the user’s identity.
Registry push using the user’s credentials.
Repository access scoped to what the user can see.
The user’s forge token is obtained during OAuth login and associated with their session. When a webhook triggers a run, the server resolves the repository owner’s token from the session store.
Open questions:
Persistence: session-only (lost on server restart) vs stored (encrypted at rest). Session-only is simpler but requires re-login after restart.
Token scope: the OAuth token may not have registry push scope. Users might need to grant additional permissions or provide a separate registry token.
Layer 3: Workflow secrets context#
A secrets expression context, matching the GitHub Actions model:
steps:
- name: Push image
run: skopeo copy ... --creds ${{ secrets.REGISTRY_TOKEN }}
Secrets are stored server-side, scoped per repository or organization. They are injected into the run environment at execution time and automatically added to the masking list.
Storage: a simple encrypted key-value store per repository.
Could use the existing TokenStore patterns (file-based,
permission-checked) initially. A proper secret management backend
(Vault, SOPS, k8s secrets) is a future option.
API: POST /api/repos/{repo}/secrets to create/update,
DELETE to remove. Secrets are write-only — the API never returns
values, only names.
Multi-user: secret access follows repository permissions. A user who can trigger runs on a repository can use its secrets. A user who cannot see the repository cannot enumerate or use its secrets.
Registry Credentials#
Pushing container images to a forge registry is a specific instance
of workflow secrets. The workflow step uses skopeo copy or
crane push with credentials from the secrets context.
For the common case of pushing to the same forge that triggered the build, the per-user forge token (layer 2) may suffice — if the OAuth scope includes registry write access. For external registries or tokens with specific scopes, workflow secrets (layer 3) are needed.
Example workflow:
jobs:
build:
runs-on: [nix]
steps:
- uses: actions/checkout@v4
- name: Build image
run: nix build .#server-image
- name: Push to registry
run: |
./result | skopeo copy \
docker-archive:/dev/stdin \
docker://${{ secrets.REGISTRY_HOST }}/myci/server:latest \
--dest-creds ${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}
Incremental Path#
Layer |
What it enables |
Depends on |
|---|---|---|
Masking |
Safe log output |
Nothing |
Per-user tokens |
User-scoped forge access, registry push via forge token |
OAuth login (done) |
Workflow secrets |
Arbitrary secrets in workflows |
Secret store, API, multi-user scoping |
Each layer is independently useful. Masking is a prerequisite for the others in the sense that without it, injected secrets would leak into logs.