Chapter 07Lesson 04~165 minutes

Secrets, Environments, Variables, Redaction, and Credential Hygiene: Diagnostics, Failure Modes, and Production Practices

Secret failures should be diagnosed by trust and lifecycle layer, not by immediately creating a broader credential. “Secret is empty,” “deployment cannot authenticate,” and “a value appeared in logs” have different causes: event restrictions, scope/precedence, environment protection, reusable-workflow contracts, runner process handling, masking limitations, or an external provider that still accepts an old credential.

DiagnosticsFork safetyArtifacts and cachesIncident responseCredential hygiene

Learning objectives

  • Diagnose missing secrets from event trust, scope, environment, name, timing, and reusable-workflow boundaries before editing permissions.
  • Explain why Base64/encoding, log masking, artifacts, caches, and command-line arguments are different leakage surfaces.
  • Handle a suspected leak by preserving safe evidence, revoking at the issuer, rotating GitHub state, and launching fresh work.
  • Recognize unsafe fork/pull_request_target, secrets: inherit, and self-hosted-runner patterns.
  • Use an evidence-first failure sequence that never prints the secret or Authorization material.

1. Diagnose the layer before changing the credential

Symptom Likely layer First evidence
Secret expression is empty Name/scope/event/reusable-workflow/environment availability Event, actor/fork, job environment, workflow contract, secret name—not value.
Secret present but provider returns 401/403 External credential validity/scope/trust policy Provider request ID/status, credential generation metadata, target identity.
Job waits before credential use Environment protection Deployment/environment approval state.
Encoded fragment appears in log Masking/transform handling Which transformation occurred; rotate if a real credential was exposed.
Artifact contains config with token Packaging/data-retention boundary Artifact manifest/path and exact producing run; remove artifact and revoke credential.

2. Empty secret: do not “fix” it by broadening the trigger

When ${{ secrets.NAME }} resolves to empty, check:

  1. Was the name created at the expected scope?
  2. Is the repository selected by organization secret policy?
  3. Does the job reference the environment that owns the environment secret?
  4. Is the run from a fork or Dependabot, where normal Actions secrets are unavailable?
  5. Is this a reusable workflow that never received the named secret?
  6. Was a same-name lower-scope secret shadowing the expected value?

Do not move code to a privileged trigger or copy the secret into source to make the symptom disappear.

3. Intentionally broken example: the environment secret is missing

# Broken on purpose: LAB_ENV_CREDENTIAL belongs to environment "lab-gate",
# but this job does not reference that environment.
permissions: {}
jobs:
  broken_consumer:
    runs-on: ubuntu-24.04
    env:
      LAB_ENV_CREDENTIAL: ${{ secrets.LAB_ENV_CREDENTIAL }}
    steps:
      - run: |
          set -euo pipefail
          if [[ -z "$LAB_ENV_CREDENTIAL" ]]; then
            echo 'diagnosis=environment_secret_not_available_to_this_job'
            exit 30
          fi

Preserve the failed run. The repair is not to duplicate the environment secret at repository scope. If the job truly owns the environment side effect, add environment: lab-gate; if it does not, the secret should remain unavailable.

4. Printing, encoding, and masking are not credential controls

Never use echo, set -x, shell tracing, whole-context dumps, or verbose HTTP tooling that can reveal headers/arguments. Base64 is not encryption. Hashing a low-entropy secret can also aid guessing and creates unnecessary derivative material.

If a real credential appears in logs, treat it as exposed.

Do not rely on deleting or hiding the log as the primary response. Revoke/rotate the credential at its issuer, update GitHub storage, cancel stale work as appropriate, and preserve only sanitized incident metadata.

5. Artifacts and caches are not secret stores

Masking affects logs; it does not scrub files. A .npmrc, cloud config, private key, kubeconfig, signing file, or generated environment file can be accidentally captured by broad artifact globs. Caches may also persist across runs within their scope.

# Safer pre-upload inspection pattern — paths only, no file contents.
set -euo pipefail
find build-output -maxdepth 3 -type f -printf '%P\n' | sort

# Fail if obvious credential-bearing filenames were accidentally staged.
if find build-output -type f \( -name '*.pem' -o -name '.npmrc' -o -name 'kubeconfig*' \) | grep -q .; then
  echo 'artifact_preflight=blocked_sensitive_filename'
  exit 31
fi

Filename checks are only one guard. Production pipelines should define positive artifact allowlists and scanner/policy controls appropriate to the data.

6. Process arguments and temporary files can bypass log hygiene

Where a tool supports it, pass credentials through environment variables or stdin instead of interpolating them into the command line. If a credential file is unavoidable, create it only inside the credential-bearing job, restrict permissions, and delete it in an always() cleanup step.

- name: Consume a fake credential through stdin
  env:
    LAB_REPO_CREDENTIAL: ${{ secrets.LAB_REPO_CREDENTIAL }}
  run: |
    set -euo pipefail
    test -n "$LAB_REPO_CREDENTIAL"
    printf '%s' "$LAB_REPO_CREDENTIAL" | python3 -c \
      'import sys; data=sys.stdin.read(); assert data; print("credential_received=true")'

The child process receives the value on stdin and prints only a boolean result. It never writes the credential to a command-line argument or log.

7. Untrusted code and secrets must not share a trust zone

A fork pull request should be testable without production credentials. Never “solve” missing secrets by using pull_request_target and checking out/executing the fork's head code with base-repository secrets. Likewise, do not route untrusted PR code to a trusted self-hosted runner that has network or host access to privileged systems.

8. secrets: inherit is a trust expansion

When a reusable workflow is called with secrets: inherit, all secrets available to the caller can become available to the directly called workflow. If that workflow only needs one registry token, passing every secret increases blast radius and obscures the interface.

Before inheritance, inventory the callee source/ref, permission contract, nested calls, actions/scripts, and exact credential need. Prefer explicit names.

9. Incident sequence for suspected exposure

  1. Stop/cancel unsafe work without destroying first-failure evidence.
  2. Identify the exact credential, repository/environment scope, run ID/attempt, source SHA, job, and exposure surface.
  3. Revoke or rotate at the external issuer first when continued misuse is possible.
  4. Update/delete the GitHub secret and any provider-side trust/access policy.
  5. Remove unsafe artifacts/caches according to authorized retention/incident policy.
  6. Fix the workflow boundary, then execute a fresh controlled run.
  7. Record why the new design prevents the same path, not merely that the next run is green.

10. Production anti-patterns

  • Printing or Base64-encoding credentials because “GitHub will mask them.”
  • Copying secrets into vars, workflow YAML, issue text, artifacts, or cache keys.
  • Sharing one cloud admin credential across build, test, publish, and deploy jobs.
  • Passing all secrets with secrets: inherit without a reviewed contract.
  • Running untrusted code with secret-bearing pull_request_target or trusted self-hosted infrastructure.
  • Keeping long-lived cloud keys where well-constrained OIDC is supported.
Next lesson

Checkpoint: prove the whole credential lifecycle

Create, use, inspect, rotate, revoke, and diagram a synthetic credential without ever exposing its value.

Knowledge check

A secret is empty in a fork PR. Should you switch to pull_request_target and run the fork code with secrets?

Why can a secret still leak even if GitHub masks it in logs?

What is the right repair when an environment secret is missing from a job that truly owns the environment side effect?

What should you do first after a real external credential appears in a log?

Why is secrets: inherit a security decision rather than a convenience keyword?

Official references and version notes

  • Secrets concept — current model for repository, organization, and environment Actions secrets and how workflows receive them.
  • Secrets reference — current naming, precedence, size/count limits, queue/start read timing, and automatic redaction behavior.
  • Using secrets in GitHub Actions — current workflow injection patterns, missing-secret behavior, fork/Dependabot restrictions, masking guidance, CLI setup, and OIDC recommendation.
  • Variables concept — current distinction between non-sensitive configuration variables and secrets.
  • Variables reference — current vars scope, precedence, limits, and environment-variable semantics.
  • Deployments and environments — current environment secret availability, protection-rule timing, plan/visibility boundaries, and self-hosted-runner warning.
  • Events that trigger workflows — current fork pull-request secret restrictions and read-only token behavior.
  • Dependabot on GitHub Actions — current Dependabot-specific token and secret restrictions.
  • Workflow syntax — current secrets, environment, reusable-workflow secret passing, and conditional semantics.
  • Workflow commands — current ::add-mask:: behavior and environment-file commands.
  • Secure use reference — current guidance for rotating/removing secrets, approval gates, untrusted input, and credential hygiene.
  • OpenID Connect — current model for exchanging GitHub OIDC identity for short-lived provider credentials instead of storing long-lived cloud secrets.
  • Reuse workflows — current explicit secret passing, secrets: inherit, and directly-called workflow boundaries.
Version and compatibility note

Version-sensitive secrets/environment behavior was rechecked against current primary GitHub documentation on 2026-09-09. Mandatory executable workflows use a learner-owned disposable repository, ubuntu-24.04, built-in Bash/Python only, synthetic credentials, and permissions: {}; no third-party action, PAT, real API key, cloud account, package publication, deployment, or self-hosted runner is required. At verification time, Actions secrets can exist at organization, repository, or environment scope; a lower scope wins on a same-name collision (environment over repository over organization). Organization/repository secrets are read when a workflow run is queued, while environment secrets are read when the job referencing that environment starts. A secret that is not set resolves to an empty string; normal Actions secrets are not passed to fork-origin pull-request workflows (except the special automatic GITHUB_TOKEN behavior) and are not available to Dependabot-triggered runs. Secrets are limited to 48 KB; current counts are up to 1,000 organization, 100 repository, and 100 environment secrets. GitHub log redaction is a safety layer, not a confidentiality proof; transformed/structured values, artifacts, caches, process arguments, and third-party systems remain separate leakage surfaces. For cloud providers that support OIDC, the course recommends short-lived federation instead of long-lived provider keys; the full OIDC trust-policy implementation is deferred to Chapter 20. Broken examples are deliberately inert/local and never contain a real credential. The lesson treats any real log exposure as an incident requiring issuer-side rotation/revocation rather than relying on redaction.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.