Module 4: Secrets Environments And Identity

3. GitHub Secrets: repo-scoped and environment-scoped

Description

act --secret-file .secrets, the mechanism you've used since Module 2, exists because it simulates something GitHub already solves natively in a real repository: Secrets, values managed from GitHub's web interface, never written in a file Git tracks, injected into a workflow only at the moment it runs. This lesson shows you where that mechanism lives on github.com —the exact click-path— how it's referenced inside a workflow with the secrets.<NAME> syntax, and the distinction between a repository Secret (available to any workflow in the repo) and an environment Secret (available only when a job declares that specific environment:) — the direct preview of what lesson 6 builds in depth.

Connection to the module

Lesson 2 explained why a credential should never live in a committed file. This lesson builds, on GitHub's side, the right place it should actually live. You already know half of this mechanism from Module 2, lesson 7: act --secret-file .secrets reads a local file and exposes it to the secrets context inside the workflow, exactly as GitHub would expose a real Secret configured on the web. What this lesson adds is GitHub's side that act is simulating: where those values get configured when the repository is real, and why the same name —secrets.AWS_ACCESS_KEY_ID— works without changing a single line of YAML between your local lab and a real production repository.


Analogy: the office locker, with two levels of key

Think of a repository Secret as an office's general locker: any employee with building access can open it, no matter which project they're working on that day. An environment Secret is different: it's a locker that only opens when you're specifically assigned to the "production" project — if you're working on "development," that locker doesn't even show up in your list of options, even though you have general building access. The dev credential and the prod credential can be named the same (AWS_ACCESS_KEY_ID in both cases) without colliding with each other, because each lives in its own locker, visible only to whoever's working in that specific environment.


Where Secrets live in a real repository: the click-path

This describes GitHub.com's real interface — there's no way to run this navigation with act, because it's platform configuration, not a workflow. If you ever create a real GitHub repository (optional, never required to complete this guide — Module 8 returns to this), this is the exact path:

   github.com/your-username/andes-cargo-infra
   └── Settings (repository tab, requires admin permission)
       └── Secrets and variables (left sidebar menu)
           └── Actions
               ├── Repository secrets   ← available to ANY workflow in the repo
               │   └── [New repository secret]
               │       Name:  AWS_ACCESS_KEY_ID
               │       Value: ●●●●●●●●●●●●●●●● (never visible again after saving)
               │
               └── Environment secrets  ← available ONLY if the job declares that environment
                   └── (requires having created an Environment first — lesson 6)

Two details verified against GitHub's real behavior, important for what follows: a Secret, once saved, cannot be read back from the interface — it can only be overwritten with a new value or deleted. GitHub doesn't even show it to you, the administrator who created it. And Secrets never show up in a run's logs by default: if a workflow accidentally printed a Secret's value with echo $AWS_ACCESS_KEY_ID, GitHub Actions detects it and automatically masks it in the log with *** — the same reason Module 2, lesson 7, taught you to print the secret's length, not its value: it's the right habit, even though act on your machine doesn't apply that automatic masking the same way real GitHub does.


How it's referenced inside a workflow: the secrets.<NAME> syntax

You already used this syntax, without formally naming it, since Module 2, lesson 7. The secrets context is one of the special variables GitHub Actions exposes inside a ${{ }} expression —from the same family as github.event (Module 2, lesson 6) or github.run_id— reserved specifically for reading a configured Secret's value, regardless of whether it comes from the real web interface or, in your case, from act --secret-file.

Andes Cargo's real ci.yml, before this lesson, had the dummy credentials written directly:

env:
  AWS_ACCESS_KEY_ID: test
  AWS_SECRET_ACCESS_KEY: test

This module's lesson 7 makes the real, executed change, but the syntax you're going to write is this:

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

secrets.AWS_ACCESS_KEY_ID tells GitHub Actions —or act, simulating it— "look for a Secret named exactly AWS_ACCESS_KEY_ID and substitute its value here, at the moment this job starts." The name inside the parentheses on the right side (secrets.AWS_ACCESS_KEY_ID) has to match, letter for letter, the name you configured in Settings → Secrets and variables → Actions (or, in your case, the name to the left of = in .secrets) — the same syntax mistake Module 2, lesson 7, already warned you about still applies here unchanged.


Repo-scoped vs. environment-scoped: lesson 6's preview

A repository Secret (Repository secrets) is available to any job in any workflow in that repository, with no additional condition. It's the type of Secret this module uses in lessons 7 and 8, because ci.yml —the plan-on-every-PR workflow— doesn't need to distinguish between dev and prod: it runs the same way, against the same LocalStack, regardless of the branch.

An environment Secret (Environment secrets) only exists inside a named Environmentdev, prod, whatever name you want— and only gets injected into a job that explicitly declares environment: <name>. This allows something a repository Secret can't: two different values with the same variable name, isolated from each other. A real example from Andes Cargo itself, which lesson 6 builds in depth: the apply.yml job running against prod could read a completely different AWS_ACCESS_KEY_ID than the same job running against dev —without the YAML changing a single line— simply because each Environment resolves the name secrets.AWS_ACCESS_KEY_ID against its own locker.

   Repository secrets                    Environment secrets

   AWS_ACCESS_KEY_ID = single-key         environment: dev
   (same value for                          AWS_ACCESS_KEY_ID = dev-key
    any job, any                           environment: prod
    environment)                             AWS_ACCESS_KEY_ID = prod-key
                                          (same NAME, DIFFERENT values,
                                           isolated per environment)

This module, in lessons 7 and 8, uses repository Secrets (the equivalent of .secrets with no environment distinction) because ci.yml doesn't need that separation — but lesson 6 shows the complete Environments mechanism, including this scoping capability, applied to the real case where it does matter: apply.yml, which, if it ran against a real AWS account, would need to distinguish dev from prod.


Common mistakes

Expecting to be able to "read back" an already-saved Secret (expectation-based). What happens: someone configures a Secret in GitHub's interface, and weeks later wants to confirm its exact value by going back into Settings → Secrets and variables. Why it happens: most configuration forms do show the saved value when you reopen them. How to spot it: if you're looking for a "show value" button or similar on a Secret's edit screen, and can't find it. How to fix it: it's a deliberate GitHub design decision, not a limitation — a Secret can only be overwritten or deleted, never read back, not even by whoever created it. If you need to confirm what value it's configured with, the only way is the one you already used in Module 2, lesson 7: a step confirming its length, never its content.

Confusing the environment variable's name with the Secret's name (syntax-based, already warned about in Module 2 and repeated here because it remains this syntax's most common mistake). What happens: someone writes env: { AWS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} } —with a different name to the left of :— and then a step uses $AWS_ACCESS_KEY_ID instead of $AWS_KEY, expecting it to work because "it's the same Secret." How to spot it: the environment variable comes out empty inside the step, with no explicit GitHub Actions or act error. How to fix it: the name to the left of : in env: is the operating system environment variable's name inside the container — it can be named whatever you want. The name inside secrets.<NAME> is the configured Secret's name, and it has to match exactly what's in Settings → Secrets and variables (or in .secrets). They're two different namespaces that, by convention and to avoid exactly this mistake, this guide always keeps identical.


Exercises

Exercise 1 — Trace the click-path from memory. Without looking back at this lesson, write the complete click-path, in order, to reach the screen where a repository Secret is configured on github.com.

See solution

Settings (repository tab) → Secrets and variables (left sidebar menu) → ActionsRepository secrets tab → New repository secret button. Requires administrator permission on the repository.

Exercise 2 — Decide between a repository and an environment Secret. For each case, indicate which one you'd use and why: (a) an external API token that uses the same value regardless of which branch or environment the workflow runs on; (b) the AWS key apply.yml needs to deploy against the prod account, different from the one it would use against dev.

See solution

(a) Repository Secret — there's no reason to distinguish by environment if the value is always the same; unnecessarily adding an Environment only adds complexity with no benefit. (b) Environment Secret, one configured inside the prod Environment and a different one inside the dev Environment — it's exactly the use case that justifies environment Secrets existing: the same variable name (AWS_ACCESS_KEY_ID), resolved to a different value depending on which environment: the job declares, without having to invent names like AWS_ACCESS_KEY_ID_PROD and AWS_ACCESS_KEY_ID_DEV in the YAML.

Exercise 3 — Explain automatic log masking. A colleague, reviewing a real GitHub Actions run's log, sees AWS_ACCESS_KEY_ID: *** instead of the real value, even though the step just did echo $AWS_ACCESS_KEY_ID. What's happening, and why doesn't this replace the habit of printing only a secret's length, as Module 2 taught?

See solution

GitHub Actions automatically detects when a configured Secret's value shows up in a log's output, and substitutes it with *** before showing it to you — a real protection, not a coincidence. But this protection has limits: it only masks the Secret's exact value as stored; if a step transforms the value somehow (encodes it in base64, concatenates it with other text, splits it into parts), the automatic masking might not recognize it. That's why keeping the habit of printing only the length —never relying solely on the platform's automatic masking— remains the safest practice, no matter how reliable GitHub's protection is.


Summary and next step

In this lesson you saw where a Secret lives in a real GitHub repository (Settings → Secrets and variables → Actions), the exact syntax a workflow uses to reference it (secrets.<NAME>, the same one act --secret-file already simulated since Module 2), and the distinction between repository Secrets (a single value, always available) and environment Secrets (different values, isolated by Environment) — the direct preview of lesson 6.

Before moving on you should be able to: trace the complete click-path to configure a repository Secret; explain why a Secret, once saved, can't be read back; and distinguish when a real case needs a repository Secret versus an environment one.

You have the vocabulary and mechanics of Secrets. Lesson 4 takes the next conceptual step: what if, instead of storing a credential at all —not in the YAML, not in a Secret— the pipeline could prove its identity with no key at all to store? That's OIDC federation.

Resources

  1. GitHub Docs — Using secrets in GitHub Actions — complete official documentation for Secrets, including creation, size limits, and automatic log masking.
  2. GitHub Docs — Security hardening for GitHub Actions — the platform's security-practices index, also referenced in this module's lesson 1.
  3. This guide's Module 2, lesson 7 (07-hands-on-passing-secrets-to-act.md) — the act --secret-file mechanism this lesson connects to its real GitHub equivalent.