Course outline · 0% complete

0/29 lessons0%

Course overview →

Secrets and environments

lesson 8-2 · ~11 min · 23/29

Where credentials live

To run docker push, the pipeline needs a registry password. It cannot go in the workflow file, because that file is in git, and anything ever committed is effectively public forever. Lesson 3-3 kept .env out of images for exactly the same reason.

CI systems solve this with secrets, values stored encrypted in the repo settings, injected into the workflow at run time, and masked as *** if a log tries to print them.

      - name: Log in to registry
        run: docker login ghcr.io -u ada -p "${{ secrets.REGISTRY_TOKEN }}"
      - run: docker push ghcr.io/ada/shop:${{ github.sha }}
PropertyA committed passwordA repo secret
in git historyyes, forevernever
visible in logsyesmasked
rotationrewrite historychange one setting

${{ secrets.REGISTRY_TOKEN }} uses the same expression syntax as github.sha in lesson 7-3, and it reads from the encrypted store. The value exists only on the runner, and only during the run.

repo secrets encrypted at rest injected at run time runner value exists only during the run git history never reached build logs printed as ***
Where a credential lives and where it does not. The encrypted store hands the value to the runner for one run, and nothing about it enters git or the logs.

Environments

Apps also need non-secret config that differs per place they run. Staging, the rehearsal copy of production, points at a test database, while production points at the real one.

The mechanism is the one wired up in lesson 5-2: environment variables, set differently per environment while the image stays identical.

jobs:
  deploy:
    environment: production
    steps:
      - run: ./deploy.sh
        env:
          DB_HOST: ${{ vars.DB_HOST }}
EnvironmentDB_HOST resolves to
stagingthe test database host
productionthe real database host

GitHub Actions formalizes this with named environments, each carrying its own variables and secrets. Same workflow, same image, and the value changes with the environment the job targets. The next two blocks cover the bash side of this.

Reading config from the environment

export sets an environment variable in the shell, and the script reads whatever the environment provides, exactly how a container or CI step receives its config.

export APP_ENV="production"
export PORT="8080"
echo "starting app in $APP_ENV mode on port $PORT"

Output

starting app in production mode on port 8080
VariableSet byRead by
APP_ENVthe environmentthe app
PORTthe environmentthe app

The distinction export makes is visibility to child processes. Without it, the variable exists in the current shell only, which is why a script that runs another program must export the values that program needs.

Defaults for missing config

Robust scripts supply a fallback with ${VAR:-default}, so a missing variable does not become an empty string.

echo "db host: ${DB_HOST:-localhost}"
export DB_HOST="db.internal"
echo "db host: ${DB_HOST:-localhost}"

Output

db host: localhost
db host: db.internal
State of DB_HOSTExpansion
unsetlocalhost
set to db.internaldb.internal

${DB_HOST:-localhost} expands to the variable when it is set and to the literal text otherwise. The colon in that form also treats an empty value as missing, which is usually what a deploy script wants, since an empty host is no more useful than an absent one.

Why a temporary committed password is not temporary

The objection is that anything committed to git stays in history even after deletion, so the value must be a repo secret referenced as ${{ secrets.NAME }}.

Git never forgets. A later delete leaves the value in history for anyone who ever gets repo access, including every existing clone and fork.

StorageRemovable later
committed in ci.ymlonly by rewriting history, and clones keep it
repo secretyes, one settings change

Secret stores keep the value encrypted, out of history, injected only at run time, and masked in logs. Temporarily committed credentials are how real breaches start, and the correct response to the discovery is always to rotate the credential rather than to delete the line.

What the encrypted store holds

They are secrets, read in a workflow with ${{ secrets.REGISTRY_TOKEN }}.

They live encrypted in the repository or environment settings, never in git history, and the runner masks them as *** if a log tries to print one.

ValueBelongs in secrets
registry tokenyes
deploy keyyes
third-party API keyyes
a database hostnameno, that is a plain variable

Anything that grants access belongs there, and rotating one is a single settings change instead of a git-history scrub. Non-sensitive config stays in variables, which keeps the secret list short enough to audit.