MLOpsThe MLOps lifecycle · MLflow 3.17 · DVC 3.67 · FastAPI · Docker · GitHub Actions · Evidently · Python 3.11+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
50 small wins to finish your pathNext lesson →

Secrets and what never goes in git

A repository secret is an encrypted value stored in a repository's settings that a workflow reads at run time as ${{ secrets.NAME }}, so a token or a password reaches the job without ever being written into a file that git tracks.

Last updated: 19 Oct, 2026 · MLOps

The pipeline built in Building the image in CI and Deploying with a self-hosted runner logs in to a registry and talks to a server. Both need a credential, and the workflow file that uses them is in the repository where anyone with access can read it. This lesson is about where those values live instead, and what to do on the day one of them was committed by mistake.

A diagram of one safe place for a credential and four unsafe ones. On the left, repository or environment secrets in the repository settings, which are write-only: you can replace a value but cannot read it back. An arrow labelled injected per run leads to the running job, which reads it as the secrets dot NAME expression, and a dashed arrow leads on to a panel saying a value printed by a step comes out as three asterisks in the log, though the docs call that masking not guaranteed. Below, in red, the four places the same value must never be written: in the workflow file, because the YAML is in git so the value ships with it; in the image, because docker history shows a build argument or an ENV; in a committed .env file, which belongs in .gitignore and .dockerignore first; and in a print or a log, because a log is kept, searched and often forwarded. A closing panel says that once a credential has been committed, deleting the line does not help, because git keeps the old commit, so the credential must be revoked at the provider and a new one issued.
<code>secrets.DOCKER_PASSWORD</code> is a reference, and the value is an access token · from the MLOPS Tutorial- Automating Workflow Of CI/CD for Dockerized Flask App Using Github Action · 24:57 to 25:57

Storing a value and reading it in a step

A secret is added in the repository's Settings page, under Secrets and variables and then Actions. Once saved it cannot be read back through the interface, only replaced or deleted. A workflow reaches it through the secrets context, passed to an action as an input or to a command as an environment variable.

yaml
      - name: Log in to Docker Hub
        uses: docker/login-action@v4
        with:
          password: ${{ secrets.DOCKERHUB_TOKEN }}   # as an action input

      - name: Call the deployed service
        env:
          API_TOKEN: ${{ secrets.SERVICE_TOKEN }}    # as an environment variable
        run: ./scripts/smoke_test.sh
  • Nothing is passed by default. A step sees a secret only because the workflow named it, which is why a long workflow can be read to find out exactly which steps touch which credential.
  • A name is not a secret. A registry username, an AWS region, a repository name: these go in repository variables, read as ${{ vars.NAME }}, which keeps the secrets list short enough to audit.
  • The rules on names. A secret name cannot start with GITHUB_ or with a number, and it is stored in upper case however it was typed. One secret holds up to 48 KB, and a repository holds up to 100 of them.

Repository secrets against environment secrets

A repository secret is available to every workflow in the repository. An environment secret belongs to a named environment, such as production, and a job reaches it only when the job declares that environment, which is also where a required reviewer or a wait timer can be attached.

yaml
  deploy:
    runs-on: self-hosted
    environment: production      # the job now sees production's secrets
    steps:
      - run: echo "deploying"
Repository secretEnvironment secret
Who can read itAny workflow in the repositoryA job that declares that environment
Typical useA registry token used by every buildThe production credential
Approval before useNoOptional: required reviewers on the environment
How manyUp to 100 per repositoryUp to 100 per environment

Keeping the value out of the repository and out of the image

A credential on a developer machine lives in a .env file, and two ignore files keep it from travelling. .gitignore keeps it out of commits; .dockerignore keeps it out of the build context, so a COPY . . line cannot bake it into a layer. Write both before the first commit, because an ignore rule added later does nothing about a file already tracked.

text
.env
.venv/
artifacts/
mlruns/
__pycache__/
text
.env
.git/
.venv/
logs/
__pycache__/

An image is the other place a credential hides. A value passed as a build argument or set with ENV stays in the image metadata, which docker history prints for anyone who pulls it, so a container takes its credentials at run time through -e or an environment file instead.

What to do when a key has been committed

Removing the line and committing again does not help. Git keeps every commit, so the value is still in the history of every clone, every fork and every build cache that pulled it. Treat the credential as public from the moment it was pushed and work in this order.

  • Revoke it at the provider first. Delete the token or the access key in the registry, the cloud console or the service that issued it. A revoked credential in a public commit is a dead string.
  • Issue a new one and store it as a secret, then update the workflow to read it from secrets.
  • Then clean the repository: remove the file, add it to .gitignore and .dockerignore, and rebuild and republish any image that carried it.
  • Check what the credential touched while it was exposed: the provider's own activity log is the place to see whether anyone else used it.

Replacing long-lived cloud keys with a short-lived one

An AWS access key stored as a secret works, and it is a key that stays valid until someone revokes it. The aws-actions/configure-aws-credentials action recommends OpenID Connect instead: the job asks GitHub for a signed identity token, AWS trades it for credentials that expire, and no long-lived key is stored anywhere. The job has to be allowed to request that token, which is what the permissions block does.

yaml
permissions:
  id-token: write                  # lets the job request GitHub's identity token

jobs:
  push-image:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: <THE ROLE ARN YOU CREATED IN AWS>
          aws-region: <YOUR REGION>

Where a value is allowed to live

ValueWhere it belongsWhy not in the repository
A registry access tokenA repository secretThe workflow file is readable by everyone with access
A production database passwordAn environment secretOnly the production job should ever see it
A cloud access keyBetter still, no key: OIDC and a roleA stored key stays valid until revoked
A registry username, a regionA repository variableIt is configuration, not a credential
A local development keyA .env file, ignored by git and DockerA tracked file travels into every clone and image
Watch out. GitHub redacts secrets it knows about from the logs, and its own documentation calls that redaction not guaranteed: a value your step reshapes, splits or encodes can come out readable. So never echo a credential to check it, never put one in a URL that a client logs, and never store one wrapped in JSON or YAML, which the same documentation advises against because the pieces are harder to match and redact.
Try it yourself
  • Store a harmless value, never a real credential, as a repository secret, echo it in a step, and look at what the log prints in its place.
  • Move one credential from a repository secret to an environment secret and watch a job without environment: stop seeing it.
  • Open your .dockerignore and check that every file holding a credential is listed in it.

Slow is fine. Stopping is the only problem.