Chapter 07Lesson 01~145 minutes

Secrets, Environments, Variables, Redaction, and Credential Hygiene: Core Concepts and Mental Model

Chapters 01–06 established execution, triggers, expressions, dataflow, and token permissions. Chapter 07 adds a different security boundary: credentials that are intentionally supplied to a workflow. A secret is not “safe because it is in the secrets context.” Its security depends on who owns it, which repository/environment can request it, which event is trusted, where the value is injected, what code can read it, which logs/files/services may retain it, and how quickly it can be rotated or revoked.

SecretsTrust boundariesScopesMaskingRotation

Learning objectives

  • Model a credential from owner/store through scope policy, workflow eligibility, job injection, external use, evidence, rotation, and revocation.
  • Distinguish Actions secrets from non-sensitive vars, ordinary env, GITHUB_TOKEN, and future OIDC identities.
  • Explain repository, organization, and environment secret scopes, precedence, and read timing without exposing secret values.
  • Treat log masking as defense in depth rather than proof that transformed values, artifacts, caches, arguments, or files are safe.
  • Identify fork/Dependabot and reusable-workflow trust boundaries before any credential is injected.

1. The real problem is credential lifecycle, not secret syntax

A workflow often needs an identity that GitHub itself does not own: a registry password, SaaS API token, signing key, database credential, or provider key. Putting the value in repository settings solves only one storage problem. It does not answer who may use the value, which event is trusted, whether a called workflow or action sees it, whether it leaks through a process argument, or how incident response revokes it.

Chapter 06 treated GITHUB_TOKEN as an automatically issued repository capability. This chapter treats user-managed Actions secrets as externally owned credentials whose exposure must be deliberately bounded.

2. Mental model: owner → scope → eligible job → process → external service → residue

Secret handling is a lifecycle with trust boundaries
flowchart TD
    A[Credential owner / external system] --> B[GitHub secret store]
    B --> C[Org / repo / environment policy]
    C --> D{Event + repository + job eligible?}
    D -->|No| E[No secret injected]
    D -->|Yes| F[Job receives selected secret]
    F --> G[Environment variable / action input / stdin]
    G --> H[External service request]
    G --> I[Runner files / process table / logs]
    H --> J[External audit + side effect]
    I --> K[Artifacts / caches / diagnostics?]
    J --> L[Rotate / revoke / expire]
    K --> L

Every arrow is a separate decision. A secret can be stored correctly and still be exposed by untrusted code. It can be masked in the GitHub log and still be copied into an uploaded artifact. It can be removed from GitHub and still remain valid at the external provider until that provider revokes it.

3. Secret, variable, token, and OIDC identity are different mechanisms

Mechanism Use it for Confidential? Lifecycle owner
vars.NAME Non-sensitive shared configuration such as region, feature mode, tool flag, environment label. No; variables render unmasked by default. GitHub org/repo/environment configuration.
env Passing values into runner processes for a workflow/job/step. Not inherently. Confidentiality depends on source and handling. Workflow author / runner process.
secrets.NAME Long-lived or externally managed confidential values when no stronger short-lived mechanism fits. Intended to be confidential; redaction is defense in depth. Credential owner plus GitHub scope policy.
github.token Same-repository GitHub API/actions capability. Yes; automatic ephemeral token. GitHub Actions / repository permissions.
OIDC Federated identity for a provider that can exchange GitHub claims for a short-lived credential. No long-lived provider secret stored in GitHub. GitHub identity + provider trust policy.

If a value can be public, it belongs in configuration, not in the secret store. If a cloud provider supports constrained OIDC federation, a stored long-lived cloud key is usually the weaker design. Chapter 20 will implement that trust policy in depth.

4. Scope is an authorization boundary

Actions secrets may be defined for a repository, an organization, or a repository environment. Organization secrets can be limited to selected repositories. Environment secrets are available only to jobs that reference that environment and can be placed behind environment protection rules.

Scope Good fit Important boundary
Repository Credential belongs to one repository's automation. Every eligible workflow in that repository can potentially reference the name; event/job design still controls actual injection.
Organization One managed credential must be shared across approved repositories. Use repository access policy; GitHub Free organization secrets/variables are not available to private repositories.
Environment Credential belongs to a target stage such as staging or production. Only jobs referencing the environment can receive it; approval/protection may delay access.

Environment scope is not magical process isolation. On a self-hosted runner, code with host access still has host access; environment approval does not turn that runner into a sandbox.

5. Same-name precedence and read timing affect rotation

If the same secret name exists at organization, repository, and environment scope, the lower scope wins: environment over repository over organization. Avoid casual shadowing because a reader may think one credential is used while another actually wins.

GitHub currently reads organization and repository secrets when the workflow run is queued. Environment secrets are read when a job that references that environment starts. This means a rotation performed after a run is queued may not change the repository secret already associated with that run, while an environment secret can be picked up later when its protected job begins.

6. A secret is not on the runner until you inject it

GitHub Actions can only make a named secret useful when workflow configuration passes it to a step/action—for example through an environment variable or an action input. Prefer mechanisms that keep it out of command-line arguments and generated scripts.

permissions: {}

jobs:
  verify_secret_presence:
    runs-on: ubuntu-24.04
    env:
      LAB_CREDENTIAL: ${{ secrets.LAB_REPO_CREDENTIAL }}
    steps:
      - name: Verify presence without revealing value
        shell: bash
        run: |
          set -euo pipefail
          if [[ -z "$LAB_CREDENTIAL" ]]; then
            echo 'credential_state=missing'
            exit 1
          fi
          echo 'credential_state=present'
          # Do not echo the credential, its reversible encoding, or a command line containing it.

If a referenced secret is absent, the expression resolves to an empty string. Secrets cannot be directly used in an if: expression; if conditional behavior is needed, map the secret to a job-level environment variable and test whether that environment variable is empty—without logging the value.

7. Masking protects logs; it does not secure every derivative

GitHub automatically redacts registered secrets and several known credential formats from workflow logs. You can register other sensitive values with ::add-mask::. But redaction is pattern-oriented log protection, not encryption, access control, or a data-loss-prevention guarantee.

Do not test masking with a real credential.

Use a synthetic non-secret toy string when demonstrating transformations. Base64 is reversible encoding, not encryption. A transformed value may no longer match the original masked text and can therefore appear in logs unless separately masked.

Also avoid structured secret blobs when possible; exact redaction becomes harder when individual fields or transformed fragments are emitted independently.

8. Forks and Dependabot change credential availability

For pull requests from forks, normal Actions secrets are not passed to the runner. The automatic GITHUB_TOKEN is a separate mechanism and is read-only in the normal fork pull-request model. Dependabot-triggered workflows likewise do not receive normal Actions secrets; Dependabot has a separate secret mechanism for its own use.

This restriction is a safety boundary, not an inconvenience to bypass. Never move privileged secret-consuming logic to pull_request_target and then execute untrusted fork code just to “get the secret back.”

9. Logs are only one residue surface

Surface Risk Safer pattern
Shell command line Arguments may be visible to process inspection/audit tooling. Use environment variables, stdin, or tool-native credential files with narrow permissions.
Temporary file May persist on self-hosted disk or be picked up by later steps. Create only when required, restrict permissions, delete in an always() cleanup step.
Artifact Retained/downloadable after the job ends. Never package credentials or secret-bearing config; inspect artifact manifest before upload.
Cache Performance store may be restorable by other runs within cache scope. Never use cache as a secret store.
External service Provider logs may retain headers, request bodies, or identities. Use provider-side redaction/audit policy and narrow short-lived credentials.

10. Credential lifecycle closes only after external revocation

A mature lifecycle records owner, purpose, scope, creation date, consuming workflows, rotation schedule, last use, and revocation procedure. Removing a secret from GitHub prevents future eligible jobs from obtaining it, but it does not necessarily invalidate the credential at the upstream service. Incident response must revoke or rotate at the credential issuer too.

Next lesson

Guided fake-secret lifecycle

Create synthetic repository and environment secrets, consume them without disclosure, demonstrate masking limits with a non-secret toy value, then rotate and revoke the lab credential while preserving evidence.

Knowledge check

Why is storing a credential as a GitHub secret not the end of the security design?

Which value should be stored as a configuration variable rather than a secret?

When are repository and environment secrets read?

Does masking prove a transformed secret cannot leak?

What should happen to a cloud key when the provider supports constrained OIDC federation?

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. The conceptual model deliberately separates GitHub storage/retrieval from the external credential issuer; deleting a GitHub secret and revoking the upstream credential are different operations.

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.