RizTech Academy logo
RizTech Academy
CI/CDLesson 6 of 625 min

Secrets in pipelines, handled safely

A deployment pipeline needs secrets — a registry password to push an image, a token to access the cluster, API keys for the app. Handling these wrong is one of the most common and most damaging security mistakes in software: a leaked secret can hand an attacker your infrastructure. This lesson is secrets in pipelines, handled safely — and it ties together a rule you have now met in the Dockerfile, Kubernetes and CI lessons. It closes the CI/CD module.

The one rule: secrets never live in your code

The single most important principle, repeated across this whole course because it matters so much: secret values must never be committed to Git, baked into an image, or written in a workflow file. Anything in your repository is visible to everyone with access to it — and Git keeps history forever, so a secret committed once and "removed" later is still there in the history, retrievable. A leaked secret is not a small mistake; it can be exploited within minutes by automated bots that scan public (and breached private) repositories for keys.

So the rule is absolute: no secret values in source code, ever. What lives in your code is a reference to a secret; the secret's value lives somewhere designed to hold secrets, and is injected at run time. You have now seen this rule three times — never copy .env into an image (Dockerfile), never commit a real Kubernetes Secret (config-and-secrets), and now: never write a secret in a workflow. It is the same principle each time.

GitHub Actions secrets

GitHub Actions provides a secret store. You add secrets in the repository (or organisation) settings — Settings → Secrets and variables → Actions — where they are encrypted and hidden. In the workflow you reference them through the secrets context, never their value:

      - name: Log in to the registry
        run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Deploy
        run: kubectl set image deployment/greetings greetings=$IMAGE
        env:
          KUBE_TOKEN: ${{ secrets.KUBE_TOKEN }}

${{ secrets.REGISTRY_TOKEN }} is filled in by GitHub at run time from the encrypted store; the value never appears in the workflow file or the repository. Two important protections come with this:

  • Secrets are masked in logs. If a secret value would be printed in the build log, GitHub replaces it with ***, so it does not leak through the logs. (Do not defeat this by, say, base64-ing a secret and printing it — a common way people accidentally leak one.)
  • Secrets are not passed to workflows triggered by pull requests from forks by default, so an outsider's PR cannot exfiltrate your secrets by editing the workflow. This is an important safety default to know.

Use the secrets context for every credential the pipeline needs — registry logins, cloud/cluster tokens, deploy keys — and they stay out of your code while still being available to the pipeline.

Where secrets should actually live

GitHub Actions secrets are fine for pipeline credentials, but the broader, more robust answer for application secrets is a dedicated secrets manager:

  • AWS Secrets Manager / HashiCorp Vault / GCP Secret Manager — purpose-built stores that hold secrets encrypted, control access with fine-grained permissions, audit who read what, and can rotate secrets automatically. Applications and pipelines fetch secrets from them at run time.
  • Kubernetes Secrets sourced from a manager — tools like External Secrets Operator or Sealed Secrets let a cluster get its Secrets from a manager (or store only an encrypted form in Git), so real values never sit in plain YAML (the config-and-secrets lesson's concern).

The progression of maturity: hard-coded (never), to environment variables and CI secret stores (good), to a proper secrets manager with rotation and auditing (best, for anything serious). For a foundation, know that GitHub Actions secrets handle the pipeline, and that real application secrets belong in a manager — and that the value never lives in Git.

Handling secrets safely in practice

A few concrete habits that prevent the common leaks:

  • Add secret files to .gitignore (.env, key files) so they cannot be committed by accident — the first line of defence.
  • Scan for committed secrets. Tools like git-secrets, truffleHog, or GitHub's own secret scanning detect keys that slipped into a repo; enable them.
  • If a secret is ever committed, rotate it immediately. Do not just delete the commit — the secret is compromised the moment it is pushed (bots are fast). Revoke it and issue a new one; removing it from history is secondary.
  • Give each credential the least access it needs — a deploy token that can only deploy, not read everything. If it leaks, the damage is limited (least privilege).
  • Rotate secrets periodically, and immediately when someone with access leaves.

The mindset: treat every secret as something that will eventually leak, and design so that a leak is contained (least privilege) and recoverable (rotation). Combined with the absolute rule — values never in code — this keeps the pipeline, the cluster and the app secure, which is a core part of the DevOps job.

Check your work

The one rule: secret values never in Git, images or workflow files — the repo is visible to all and Git history is forever (a "removed" secret is still there, and bots find leaked keys in minutes). Code holds a reference; the value lives in a secret store, injected at run time. Same rule as Dockerfile .env and Kubernetes Secrets.

GitHub Actions secrets: add in Settings → Secrets and variables → Actions; reference via ${{ secrets.NAME }} (value filled at run time, never in the file). Protections: masked in logs (*** — don't defeat it), and not shared with fork PRs by default.

Where secrets live: CI secret store (good, for pipeline creds); a secrets manager (AWS Secrets Manager/Vault/GCP — encrypted, access-controlled, audited, auto-rotating) for application secrets; Kubernetes Secrets sourced from a manager (External Secrets/Sealed Secrets). Maturity: hard-coded (never) → env/CI store (good) → manager (best).

Safe habits: .gitignore secret files; scan for committed secrets (git-secrets/truffleHog/GitHub secret scanning); if committed, rotate immediately (it's compromised — deleting the commit isn't enough); least privilege per credential; rotate periodically and on departures. Assume every secret will leak; contain (least privilege) and recover (rotation).

Practice

  1. State the absolute rule about secrets and code, and explain why deleting a committed secret is not enough.
  2. Store a token as a GitHub Actions secret and reference it in a workflow with ${{ secrets.NAME }}.
  3. Explain how GitHub masks secrets in logs and one way people accidentally defeat that.
  4. Explain why secrets are withheld from fork pull-request workflows by default.
  5. Compare a CI secret store with a full secrets manager, and say what each is best for.
  6. Describe what you would do the moment you discover an API key was committed to a repo.

Official documentation

Next: the Infrastructure as Code module.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship