Why CI/CD secrets management is a different problem
CI/CD pipelines need powerful credentials. A build job may need a package registry token, a cloud deployment key, a database migration password, a signing key, a Slack webhook, and an API token for production infrastructure. Those credentials often have enough access to ship code, change infrastructure, publish artifacts, or reach customer data.
That makes CI/CD secrets management one of the highest-intent security problems in software delivery. Teams are not asking in the abstract. They usually have a pipeline already running, secrets already copied into a CI provider, and a growing concern that one compromised workflow, forked pull request, debug log, or long-lived runner can expose too much.
The core problem is simple: your CI system is both a place that stores secrets and a place that runs untrusted or semi-trusted code. That combination deserves a stricter model than a static environment variable.
The limits of built-in CI provider secrets
GitHub Actions secrets, GitLab CI/CD variables, Bitbucket repository variables, and similar features are better than putting secrets in source control. They keep values out of the repo, mask common log output, and make deployment configuration easier to manage.
But provider-level secrets are still usually static values handed to jobs as environment variables. Once a job receives a secret, the CI provider can no longer control what the job does with it. A script can print it in transformed form, send it to another host, write it into an artifact, or leave it behind on a persistent runner.
The weakest parts are predictable:
- Long-lived values. A deployment token may stay valid for months because rotating it risks breaking releases.
- Broad repository scope. A secret attached to a repo or environment can be available to more workflows than the one job that truly needs it.
- Runner residue. Self-hosted runners can keep files, caches, shell history, or process state after a job ends.
- Poor actor identity. Audit logs often show a workflow or job, but not a durable cryptographic identity for the runner that fetched the value.
- Copy pressure. The more providers and environments you use, the more likely the same secret is copied into multiple dashboards.
Provider secrets are useful as a bootstrap layer. They are not a complete secrets-management model for production delivery.
What good CI/CD secrets management needs to do
A strong pipeline secrets model should answer five questions.
What is asking for the secret?
A production deployment job should not authenticate as a reusable bearer token pasted into a variable. The runner should prove its own identity for the request it is making.
Which exact secrets can it read?
A frontend build should not inherit the same secret bundle as a database migration job. Least privilege needs to work at the individual secret level, not just the repository or project level.
How long does that access live?
CI runners are often disposable. Their secret access should be disposable too. A runner that exists for one job should not leave behind a credential that works next week.
Can the read be audited by machine, secret, and source IP?
When a release pipeline behaves unexpectedly, you need to know which machine identity read which secret, when, and from where. A generic token-read log is not enough.
Can you rotate without breaking deploys?
Secrets that cannot be rotated safely become permanent infrastructure. A practical system needs versioning, rotation, and a way to move clients to the new value deliberately.
The common anti-pattern: one CI token that can read everything
Many teams centralize pipeline access by creating a single CI token for the secrets manager, storing that token in GitHub Actions or GitLab CI, and using it to fetch everything during the build.
That is easy to wire up, but it recreates the same risk in a new place. The CI token becomes the secret that protects all other secrets. If it leaks, the attacker gets whatever that token can read. If the token is broad, one compromised workflow becomes a vault-wide incident.
It also weakens audit quality. A log that says ci-token read DATABASE_URL does not tell you which runner, which generated job, or which ephemeral environment actually made the request. The more your pipelines scale, the less useful that attribution becomes.
A better pattern is to enroll each runner as its own machine identity, grant it only the projects and secrets it needs, and let that identity expire automatically.
A safer pattern for ephemeral runners
Ephemeral runners are a good fit for modern CI because every job starts from a fresh machine and disappears afterward. But the secret access model has to match that lifecycle.
The ideal flow looks like this:
- A dashboard user creates an enrollment token for a specific pipeline scope.
- The token defines which projects the runner joins, which secrets it can read, how many machines it can enroll, and how long each machine should live.
- The CI provider stores only the enrollment token as the bootstrap value.
- At job start, the runner enrolls itself and generates its own keypair.
- The runner fetches only the secrets granted by that enrollment token.
- When the job window ends, the machine identity expires and is disabled.
This keeps the permanent CI variable narrow. It is not the production database password. It is not a vault-wide read token. It is a constrained bootstrap credential that creates short-lived machine identity.
How SikkerKey handles CI/CD secrets
SikkerKey is built around machine identity rather than bearer-token retrieval. For CI/CD, that means the runner does not fetch secrets by presenting a long-lived API token as proof of identity.
A SikkerKey enrollment token can create an ephemeral machine with a specific scope: project memberships, secret grants, token lifetime, machine lifetime, max uses, and optional restrictions such as source CIDR, hostname pattern, and machine-name pattern. The token plaintext is not stored server-side; the backend stores a hash and validates the enrollment against the vault it belongs to.
When the runner enrolls, it generates an Ed25519 keypair. The private key stays on the runner. SikkerKey stores the public key and uses it to verify future requests from that machine.
Every machine secret request is signed over the method, path, timestamp, nonce, and request body hash. That means a captured request cannot be repointed at another endpoint or replayed later. The nonce is single-use, the timestamp is short-lived, and the server rejects unsigned changes to the request shape.
For CI templates, SikkerKey can generate bootstrap scripts for GitHub Actions, GitLab CI, Bitbucket Pipelines, and generic POSIX shell environments. The generated script enrolls the ephemeral machine, unlocks the granted projects, exports the granted secrets to dotenv format, and then passes them into the pipeline in the provider-specific way.
The important detail is the scope: the generated script is based on the enrollment token's policy. It does not invent access at runtime, and it does not need a reusable vault-wide token embedded in the workflow.
Per-secret grants instead of repository-wide bundles
Build pipelines are not all the same. A test job might need a package registry token and a test database URL. A deploy job might need a cloud role, a production migration credential, and a signing key. A release-notification job might need only a webhook.
SikkerKey models that distinction directly. Machines are attached to projects and explicitly granted individual secrets. If a CI runner is not granted a secret, the read is denied even if the runner is approved and authenticated.
That matters when one workflow is compromised. The blast radius is the exact set of secrets granted to that enrolled machine, not every secret stored for the repository or organization.
For higher-risk credentials, SikkerKey access policies can add another gate before decryption. A secret can require an IP allowlist, a time window, read-rate caps, a read budget, a TTL, a co-signing machine, or rotation after a number of reads. These checks happen after the normal machine authentication and grant checks, but before the secret value is decrypted.
Audit logs that help after a pipeline incident
CI incidents are often noisy. A workflow changes, a runner image updates, a dependency script runs more code than expected, or a job starts from a source branch you did not intend to trust. When that happens, the audit log has to be useful under pressure.
With SikkerKey, successful reads, denied reads, policy blocks, rotations, enrollment events, and machine lifecycle changes are recorded against concrete actors. For a secret read, the audit event can tie together the machine identity, secret, source IP, project, and time.
That is a different quality of evidence from "a CI variable existed" or "a workflow had access to an environment." You can answer which runner identity accessed which credential and whether the request passed the expected policy.
CI/CD secrets management checklist
Use this checklist when you review a pipeline:
- Remove secrets from source control and build artifacts. No
.envfiles, generated config files, or private keys should be committed or uploaded as artifacts. - Keep CI provider variables narrow. Use them for bootstrap credentials, not for every production credential.
- Prefer ephemeral runner identity. A runner that exists for one job should receive an identity that expires with that job.
- Scope by secret, not by repository. Each workflow should receive only the secrets required for its task.
- Sign requests instead of reusing bearer tokens. The runner should prove possession of a private key without sending that key over the network.
- Audit every read. Secret access should be traceable by machine identity, secret, source IP, and timestamp.
- Add policies to sensitive credentials. Use IP allowlists, time windows, read caps, TTLs, and rotate-after-read rules where they make operational sense.
- Practice rotation. A secret that cannot be rotated under normal conditions will be much harder to rotate during an incident.
GitHub Actions, GitLab CI, and Bitbucket all need the same core model
The provider changes the syntax, not the security model.
In GitHub Actions, secrets often enter later steps through environment files. In GitLab CI or Bitbucket Pipelines, teams commonly source a dotenv file during script execution. In a generic POSIX pipeline, the same pattern can work with shell exports.
Those differences matter for implementation, but they do not change the goal. The pipeline should bootstrap a short-lived identity, fetch only the secrets it is granted, expose them only to the steps that need them, and leave behind as little durable credential material as possible.
That is the model SikkerKey optimizes for: enroll, unlock the granted projects, export the granted secrets, run the job, and let the machine identity expire.
Where SikkerKey fits
SikkerKey is a secrets manager for teams that want production-grade CI/CD secrets management without operating a vault cluster or building a policy system from scratch.
For pipelines, the practical advantages are:
- Ephemeral machine enrollment for CI runners and autoscaling infrastructure.
- Ed25519 signed secret requests instead of reusable bearer-token reads.
- Per-project membership and per-secret grants.
- Envelope encryption at rest with per-secret data keys.
- Access policies for IP, time windows, rate caps, TTLs, co-signing, and rotate-after-read controls.
- Audit events tied to machine identity and secret access.
- Generated CI templates for GitHub Actions, GitLab CI, Bitbucket Pipelines, and generic shell environments.
The result is a pipeline secrets model that matches how CI actually works. Runners are temporary. Access is scoped. Reads are signed. Audit logs name the machine. And the long-lived secret sitting in your CI settings is no longer the thing that can read every production credential.
Frequently asked questions
Are GitHub Actions secrets or GitLab CI variables enough?
They are a useful baseline, but they are not enough for every production pipeline. They store static values and inject them into jobs. A secrets manager adds stronger machine authentication, central rotation, per-secret grants, policy checks, and audit logs across providers.
Should CI jobs use environment variables at all?
Environment variables are a reasonable delivery mechanism for short-lived job execution. The risk is treating static environment variables as the source of truth for long-lived production credentials. Fetch secrets at runtime, keep scope narrow, and avoid writing values into logs, artifacts, or persistent runner storage.
What is an ephemeral machine in CI/CD?
An ephemeral machine is a machine identity with an expiration time. In CI/CD, it usually maps to a runner or job environment that should exist only for a short window. When the identity expires, it is disabled and no longer allowed to read secrets.
Why are signed requests better than bearer tokens for CI secrets?
A bearer token proves that the caller has a string. If the string leaks, it can be reused until it expires or is revoked. A signed request proves possession of a private key without sending that key, and the signature is bound to a specific request with a timestamp and nonce. Capturing one request does not create a reusable credential.
How should a team start improving CI/CD secrets management?
Start with the highest-risk pipeline: production deploys, database migrations, package publishing, or artifact signing. Inventory the secrets it uses, remove any values from source control and artifacts, replace broad tokens with scoped machine identity, and make sure every read is auditable. Then repeat the pattern across lower-risk workflows.