Chapter 07Lesson 02~190 minutes

Secrets, Environments, Variables, Redaction, and Credential Hygiene: Guided Hands-On Workflow

This lesson turns the lifecycle into a safe experiment using only synthetic values. You will create one repository configuration variable, one repository Actions secret, and—where available—one environment-scoped secret in a disposable repository. Workflows prove presence without printing credentials, a non-secret toy value demonstrates why transformations can escape masking, and repeated manual runs provide rotation and revocation evidence.

Hands-onFake secretsEnvironment secretMasking demoRevocation

Learning objectives

  • Create synthetic repository and environment secret state without using a real provider credential.
  • Consume a secret through a process environment variable while logging only presence/state evidence.
  • Compare a non-sensitive vars value with a confidential secrets value in one workflow.
  • Demonstrate masking limitations using an intentionally non-secret toy string rather than exposing a credential.
  • Rotate then remove the lab secret and verify that a later workflow run observes revocation as an empty secret.

1. Scenario and authorization boundary

Use a repository created only for this course chapter. The values below are intentionally synthetic and must not authorize any real service.

State Suggested value Why
Repository variable LAB_STAGE training Non-sensitive configuration; should be visible in logs.
Repository variable LAB_SECRET_GENERATION v1 Non-secret rotation marker; can be printed safely.
Repository secret LAB_REPO_CREDENTIAL A random course-only fake string with no external authority. Exercises secret storage and injection.
Environment lab-gate Disposable environment Exercises environment association.
Environment secret LAB_ENV_CREDENTIAL A different course-only fake string. Shows environment-specific access/timing.

For a free mandatory path, a disposable public repository supports environments/environment secrets on current plans. If you deliberately use a private repository on a plan that does not expose environment secrets, complete the repository-secret workflow and use the documented environment section as a simulation; do not upgrade or expose a real project merely for the lab.

2. Create state without putting secret values in source

  1. In repository settings, create variables LAB_STAGE=training and LAB_SECRET_GENERATION=v1.
  2. Create repository secret LAB_REPO_CREDENTIAL with a synthetic value.
  3. Create environment lab-gate. Add LAB_ENV_CREDENTIAL there. Optional: add a reviewer if your repository/plan supports the protection rule and another authorized reviewer is available.
  4. Do not commit the fake secret values, paste them into issue text, or include them in screenshots.

The secret can be fake and still be handled as though it were valuable. That discipline makes the same workflow pattern safe when real credentials are later introduced.

3. Repository and environment scope in one workflow

name: Chapter 07 secret lifecycle lab
on:
  workflow_dispatch:

permissions: {}

jobs:
  repository_scope:
    runs-on: ubuntu-24.04
    env:
      LAB_REPO_CREDENTIAL: ${{ secrets.LAB_REPO_CREDENTIAL }}
      LAB_STAGE: ${{ vars.LAB_STAGE }}
      LAB_SECRET_GENERATION: ${{ vars.LAB_SECRET_GENERATION }}
    steps:
      - name: Inspect non-sensitive configuration
        run: |
          printf 'stage=%s generation=%s\n' "$LAB_STAGE" "$LAB_SECRET_GENERATION"

      - name: Verify repository secret presence only
        run: |
          set -euo pipefail
          if [[ -z "$LAB_REPO_CREDENTIAL" ]]; then
            echo 'repo_secret_state=missing'
            exit 20
          fi
          echo 'repo_secret_state=present'

  environment_scope:
    runs-on: ubuntu-24.04
    environment: lab-gate
    env:
      LAB_ENV_CREDENTIAL: ${{ secrets.LAB_ENV_CREDENTIAL }}
    steps:
      - name: Verify environment secret presence only
        run: |
          set -euo pipefail
          if [[ -z "$LAB_ENV_CREDENTIAL" ]]; then
            echo 'environment_secret_state=missing'
            exit 21
          fi
          echo 'environment_secret_state=present'

The first job receives no environment secret because it does not reference lab-gate. The second job's environment association is visible in run state; if a protection rule is active, the job waits before GitHub makes the environment secret available.

4. Evidence packet for Run A

Record only non-secret facts:

run_id: <record from Actions UI>
run_attempt: 1
source_sha: <exact workflow revision>
repository: <disposable repository>
event: workflow_dispatch
LAB_STAGE: training
LAB_SECRET_GENERATION: v1
repository_secret: present (value not recorded)
environment: lab-gate
environment_secret: present (value not recorded)
protection_review: used / not configured / simulated
external_side_effect: none

If a reviewer is configured, preserve the approval/deployment record. Do not use a screenshot that captures secret-entry forms.

5. Demonstrate masking limitations without touching the real secret

Add a separate step that uses a deliberately non-sensitive toy string. Its purpose is to show mechanics, not to prove anything about the actual secret.

- name: Safe masking demonstration with non-secret data
  shell: bash
  env:
    TOY_VALUE: course-mask-demo-not-a-credential
  run: |
    set -euo pipefail
    echo "::add-mask::$TOY_VALUE"
    echo "exact=$TOY_VALUE"
    encoded=$(printf '%s' "$TOY_VALUE" | base64 -w 0)
    echo "reversible_base64=$encoded"
    echo 'The encoded line may remain visible because Base64 changed the string.'

The exact toy value should be masked after registration; its Base64 transformation is a different string and may be visible. That is why “GitHub masks my secret” is not a license to transform and print it. Never run this demonstration with LAB_REPO_CREDENTIAL.

6. Rotate: change secret and publish a non-secret generation marker

  1. Update LAB_REPO_CREDENTIAL to a second synthetic value. Do not print either old or new value.
  2. Update LAB_SECRET_GENERATION from v1 to v2.
  3. Dispatch Run B.
  4. Record that Run B reports generation=v2 and repo_secret_state=present.

The generation variable is operator-maintained audit metadata, not cryptographic proof of the secret contents. The actual secret remains intentionally opaque. In production, rotation proof should come from the credential issuer's audit trail or successful scoped authentication—not from logging a secret-derived hash.

7. Rotation timing: queued run versus environment job start

Repository and organization secrets are read when the run is queued. Therefore, do not assume editing a repository secret changes an already queued run. Environment secrets are read when a referencing job starts, which is especially relevant if the job waits for an approval gate.

During incident response, cancel stale queued/in-progress work as appropriate, rotate at the issuer, update GitHub storage, and launch a fresh run whose evidence begins after the rotation boundary.

8. Revoke the lab secret and prove future absence

  1. Delete LAB_REPO_CREDENTIAL from the disposable repository.
  2. Dispatch Run C.
  3. The repository_scope job should fail at the explicit empty-string check with repo_secret_state=missing.
  4. Preserve Run C rather than changing the workflow to make it green.

A missing secret reference resolves to an empty string. That observable failure is the revocation evidence for GitHub-side availability. If the value represented a real provider credential, you would also revoke it at the provider; deleting the GitHub copy alone would not invalidate the external credential.

9. Cleanup and rollback

  • Delete LAB_ENV_CREDENTIAL and remove lab-gate if it exists only for this lab.
  • Delete LAB_STAGE and LAB_SECRET_GENERATION if they are lab-only.
  • Keep the workflow/run evidence if you want an audit packet; it contains no credential values.
  • Delete the disposable repository when the course experiment is complete if you no longer need the run history.

10. Challenge: choose configuration, secret, or identity

Classify each value before editing YAML: a public region name, a service URL, a registry password, a cloud deployment identity, and a per-run checksum. Use vars for public shared configuration, a secret only where a confidential stored value is genuinely required, OIDC for supported cloud identity, and ordinary outputs/artifacts for non-secret dataflow.

Next lesson

Secret architecture and trade-offs

Decide scope, identity type, reuse boundary, and approval model before a credential is copied across workflows.

Knowledge check

Why does the lab print LAB_SECRET_GENERATION but not the secret?

What should Run C observe after deleting LAB_REPO_CREDENTIAL?

Why is the Base64 demonstration performed on a toy string?

When can an environment secret become available to a protected job?

Does deleting the GitHub secret revoke a real external API key?

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 mandatory lab intentionally uses only synthetic credentials and local presence checks. The masking demonstration uses an explicitly non-secret toy value, never the stored lab secret.

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.