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.
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, ordinaryenv,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
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.
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.
Knowledge check
Why is storing a credential as a GitHub secret not the end of the security design?
Storage does not define event trust, job eligibility, process injection, action trust, logs/files/artifacts, external-service retention, or provider-side revocation.
Which value should be stored as a configuration variable rather than a secret?
A non-sensitive value such as a region, build mode, or environment label that is safe to appear unmasked.
When are repository and environment secrets read?
Organization/repository secrets are read when the workflow run is queued; environment secrets are read when the job referencing the environment starts.
Does masking prove a transformed secret cannot leak?
No. Masking is log redaction. Transformed or fragmented values can escape exact matching, and artifacts/caches/process arguments are separate surfaces.
What should happen to a cloud key when the provider supports constrained OIDC federation?
Prefer replacing the long-lived stored key with a short-lived federated credential, after defining a narrow provider trust policy.
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
varsscope, 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-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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.