Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLXXXIX · Repository SecurityRepoSecurity

Secret scanning and push protection — native controls and gitleaks

Advanced⏱ ~25 mingit

What you'll learn

  • Enable native secret scanning and push protection at repository scope and verify the setting took effect
  • Distinguish detection after the fact from prevention at the push, and explain why both are needed
  • Position gitleaks to cover the formats and hosting that native scanning does not
  • Treat a push-protection bypass as an auditable event with a defined follow-up

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Secret scanning and push protection are two different controls that share a detection engine. Scanning looks at what is already in the repository and tells you about it. Push protection sits in the receive path and refuses the push. The first produces an incident; the second prevents one. An infrastructure repository wants both, and it wants them switched on before the first credential arrives, not after.

Turning the native controls on

At repository scope on GitHub, both are one command each:

REPO=example-org/infra-prod
gh repo edit "$REPO" --enable-secret-scanning
gh repo edit "$REPO" --enable-secret-scanning-push-protection

Order matters: push protection builds on scanning, so enabling the scanner first is the correct sequence. On GitLab the equivalent controls are the secret detection analyzer, configured as a pipeline stage in the project’s security configuration, and server-side push rules that reject commits matching a pattern before they land.

Enabling at repository scope is the starting point, not the end state. On any estate above a handful of repositories, set the defaults at organisation or group level so that a repository created next week inherits the control rather than waiting for someone to remember it.

flowchart LR
    A["git push"] --> B{"push protection"}
    B -->|"pattern matched"| C["push rejected at the server"]
    B -->|"clean"| D["ref updated"]
    C --> E{"author bypasses?"}
    E -->|"yes, with reason"| D
    E -->|"no"| F["secret removed, push retried"]
    D --> G["secret scanning over history"]
    G -->|"finding"| H["alert, rotate the credential"]

The bypass edge is the one to look at. It exists because a false positive at 03:00 during an incident must not be an unbreakable wall - but every traversal of that edge is recorded with the person and their stated reason, and that record is the thing your review process consumes.

Where gitleaks belongs

Native scanning covers the patterns the forge knows about and the repositories the forge hosts. gitleaks covers what is left:

  • Self-hosted and mirrored repositories where the native scanner is unavailable or not licensed.
  • Bespoke credential formats - an internal token prefix or a private PKI key format that no vendor ruleset knows.
  • The pre-commit position, where a finding costs the engineer ten seconds instead of an incident.
  • Full-history sweeps on a schedule, over branches that are not in the pull-request path.
gitleaks detect --source . --redact --exit-code 1
gitleaks protect --staged --redact

The first is the audit form, suitable for CI and scheduled sweeps; --redact keeps the finding out of the CI log, which otherwise becomes a second copy of the secret in a system with different access controls. The second checks what is staged, which is the pre-commit hook position.

Where the credential should have gone

Every blocked push has a legitimate destination for the value that was in the diff. For a pipeline credential, that destination is the repository or environment secret store:

REPO=example-org/infra-prod
gh secret set DEPLOY_TOKEN --repo "$REPO" --body "$NEW_TOKEN"

The value is write-only from that point: the pipeline can consume it, and no one can read it back through the UI or the API. For anything a human needs to read, the destination is the team’s secret manager, not a file in the repository - and the repository holds a reference to the secret, not the secret itself.

Production discipline

  1. Enable at organisation scope, not per repository. A control that depends on someone remembering is a control with a coverage gap by construction.
  2. Rotate first, clean up later. The clock on a leaked credential starts at the push, not at the alert.
  3. Redact in CI output. A detector that prints the secret it found has moved the secret into the build log.

Cross-course references

  • Ansible for Production Sysadmins - the vault parts cover the encrypted-in-repo pattern, which changes what a scanner sees and what it cannot see.
  • Terraform for Production Sysadmins - state files contain resolved secret values; the state backend needs the same scanning posture as the repository.
  • Docker for Production Engineers - build arguments and image layers are a second place committed secrets survive after the repository has been cleaned.

Quiz

Knowledge check · 4 questions

  1. Q1. Push protection is enabled today on a repository whose history has contained a cloud access key since 2023. What is the effect on that key?

  2. Q2. Running a secret detector in CI without redaction can place the secret into a second system with different access controls.

  3. Q3. Give three coverage gaps in native secret scanning that a self-hosted detector such as gitleaks fills.

  4. Q4. Push protection was bypassed and a live database credential reached the default branch. Run the response and fix the structural cause.

    During a Friday incident, an engineer needed a hotfix merged quickly. Push protection blocked the push, flagging a string in a new configuration file. The engineer selected the bypass option, gave the reason 'false positive - test fixture', and the push succeeded. The string was the production read-write database password. It sat on `main` for six days before a scheduled gitleaks sweep raised it. The repository is public to the organisation, has 140 members with read access, and is mirrored nightly to an internal backup host.

Passing score: 75%. Answers are checked in this browser.