Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXLII · CI SecretsSecrets

Secret variables fundamentals — how CI platforms store and serve secrets

Advanced⏱ ~20 mingit

What you'll learn

  • Describe the encrypted-at-rest storage model for CI secrets and what the platform guarantees
  • Trace the access path from a workflow declaring a secret to the runner receiving it as an environment variable
  • Distinguish secret storage from secret usage and explain why the boundary matters
  • Identify what a secret is not - configuration, code, or a substitute for a credential system

Prerequisites

Practice

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A CI secret is the smallest unit of trust in a pipeline. Every cloud credential, registry token, deploy key, and signing key the runner reads passes through the secret store. The forge’s job is to keep the secret encrypted at rest and to hand it to the runner at the right moment; the team’s job is to keep the secret from being printed, persisted, or exfiltrated by the job that received it.

The encrypted-at-rest model

When a team stores a secret in GitHub Actions, GitLab CI, CircleCI, or Jenkins, the value never sits in the database as plaintext. The forge encrypts the value with a platform-managed key and stores only the ciphertext. The plaintext is materialised on demand when a workflow declares the secret and the runner receives it.

flowchart LR
    A["Engineer\nenters value"] --> B["Forge encrypts\nwith platform key"]
    B --> C["Ciphertext\nstored at rest"]
    C --> D["Job declares secret\nin workflow"]
    D --> E["Forge decrypts\ntransiently"]
    E --> F["Runner receives\nas env var"]
    F --> G["Step reads\nuses, then forgets"]

Three properties follow from this model:

  • Confidentiality at rest. A database backup, a forge insider, or a read-only replica does not yield the secret. The attacker must hold a runtime credential authorised to decrypt, not just a copy of the storage row.
  • Confidentiality in transit. The forge hands the plaintext to the runner over an authenticated channel; the runner does not pull the secret from a public endpoint. Eavesdropping on the channel requires forging the runner’s identity.
  • Confidentiality in memory. The plaintext exists in the runner’s process memory for the lifetime of the job and is released when the job ends. The forge has no control over what the job does with the value during that window.

The third property is the boundary the forge cannot cross. The runner is the team’s responsibility from the moment the value arrives.

The access path

When a workflow declares a secret, the runtime does the following:

  1. Resolve scope. The forge checks whether the workflow is authorised to read the secret. Repository secrets are scoped to a single repository; environment secrets are scoped to a named environment within a repository; organisation secrets are scoped to every repository that has not opted out.
  2. Decrypt and inject. The forge decrypts the ciphertext, sets an environment variable on the runner process, and hands the value to the job. The variable is available to every step in the job unless the workflow explicitly scopes it.
  3. Mask in the controlled sinks. The forge’s log masker (covered in lesson XLII-02) registers the value as a mask pattern; subsequent log lines that contain the exact value are replaced with *** before the log is persisted.

Inside a step, the value is just an environment variable. The job can read it the same way any shell script reads an environment variable:

echo "$DEPLOY_TOKEN"
env | grep '^SECRET_'

The shell is the access path. There is no privileged API, no per-step guard, no built-in scope check inside the job. A step that can read $DEPLOY_TOKEN can read it; the forge’s enforcement happened at job start, not at every read.

Storage is not usage

The most common failure mode is treating a CI secret as a full credential system. The forge’s secret store is a small, narrow subsystem designed for one job: handing a value to a runner at job start. The forge’s secret store is not:

  • A credential vault. A vault issues, rotates, audits, and revokes; a CI secret does none of these.
  • A secret manager. A secret manager scopes, leases, and logs every read; a CI secret hands the value to the runner and the runner’s logs are out of the platform’s hands.
  • A substitute for OIDC. A long-lived cloud credential in a CI secret is still a long-lived credential; the storage model does not change the lifetime, the scope, or the revocation model. Part XLII-06 returns to this point.

A CI secret is a delivery channel. The credential itself, the rotation cadence, and the revocation model live elsewhere.

Practical secret shape

The cleanest CI secret is a string the runner can read and the forge can mask. Three properties help:

  • Single line. The forge’s masker is a literal-string matcher; a multi-line secret cannot be masked because the matcher does not span lines reliably. Lesson XLII-02 returns to this constraint.
  • Stable. A secret that changes on every read (a rotating token, a freshly-minted nonce) cannot be masked because the masker cannot register an unregisterable pattern. Lesson XLII-05 returns to this trade-off.
  • Scoped narrowly. A secret that grants the minimum permissions the job needs; a secret that grants more permissions than the job needs is a secret whose blast radius exceeds the job’s blast radius.

The shape of a secret is itself a security decision.

Production discipline

  1. Secrets are delivery channels, not credential systems. Treat the CI secret as the envelope, not as the letter. The letter lives in a vault or in the cloud provider’s IAM; the envelope is the CI secret.
  2. Read secrets in the smallest scope that needs them. A secret scoped to an environment is a secret that a PR-triggered build cannot read; the exposure window is the environment’s lifetime, not the workflow’s.
  3. Audit secret values, not just secret names. A secret named AWS_PROD might be a fine-grained OIDC role or an account-wide AdministratorAccess key. The name is the team’s convention; the value is what the runner reads. Audit the value’s permissions.
  4. Rotate from the source, not from the CI store. When the credential rotates, the rotation happens at the source (the vault, the cloud provider, the registry). The CI secret is overwritten; the CI secret is not the system of record.

Cross-course references

  • Git, CI/CD & GitOps — Part XXXV-01 (The secret-leak fallacy) covers the principle that a secret committed once is leaked; the same principle applies to a secret printed once.
  • Git, CI/CD & GitOps — Part XXXVIII-05 (Environment variables and config) covers the runner’s environment plane the secret is injected into.
  • Git, CI/CD & GitOps — Part XLI-03 (Production credentials on runners) covers why long-lived CI secrets are the wrong default.

Quiz

Knowledge check · 4 questions

  1. Q1. Which statement most accurately describes a CI platform's guarantee for a stored secret?

  2. Q2. A CI secret stored in the forge is equivalent in security properties to a credential stored in HashiCorp Vault or AWS Secrets Manager.

  3. Q3. Describe the three properties the encrypted-at-rest model provides, and name the property the model cannot provide once the secret is in the runner's memory.

  4. Q4. Diagnose where the secret-leak boundary was crossed, and recommend the storage-model changes that would have closed the gap.

    Team T stores a long-lived AWS access key named AWS_DEPLOY_KEY in the repository's GitHub Actions secrets. The key is AdministratorAccess on the production account. A workflow runs a step that calls `curl --data 'token=$AWS_DEPLOY_KEY' https://internal.example.com/log` to record the deploy to an internal audit log. The internal service returns a 500 error whose body contains the full token in a stack trace. The error is forwarded to a third-party error-tracking service that retains logs for 30 days. A security engineer finds the token in the error-tracking service six weeks later.

Passing score: 75%. Answers are checked in this browser.