Technical overviewSecurity engineering in CI/CD

Supply Chain Gates

Secret, dependency, container and configuration scans that block a merge, each one proved by planting the defect it exists to catch.

Source code

Inspect the implementation, commit history, and project documentation.

Role
Solo engineer: workflow design, scanner configuration, allowlists, GitLab port, self-hosted runner
Evidence status
Running on three public GitHub repositories and one mirrored GitLab project on a self-hosted runner; every gate has been observed failing a build on a planted defect
Verified
2026-08-23

01

Purpose and scope

Four scans run as three jobs on every pull request and push across three public repositories: a committed credential, a vulnerable dependency, a vulnerable container image, or a misconfigured Dockerfile each fail the build before merge. The same gates are ported to GitLab CI on a self-hosted runner, and every one was verified by planting the exact defect it exists to catch.

What it is used for

  • Stop a credential from reaching a public repository, and surface those already in its history.
  • Refuse a release whose dependencies or image carry a known HIGH or CRITICAL vulnerability.
  • Catch a container or Kubernetes misconfiguration in review rather than after deploy.
  • GitHub Actions
  • GitLab CI
  • Gitleaks
  • Trivy
  • Checkov
  • Docker
  • Bash

02

Architecture

Plant a defect

A simulation of the configured gates, not a live pipeline run. Every scanner, threshold and framework below is read from the committed workflow.

Pick what to commit

pull requestmain at 7a14485

  • Secret scanfull history at fetch-depth 0, redacted, exit 1 on any leakGitleaks 8.30.1 · timeout 15 minpassed
  • Dependency and container scanfilesystem and a rebuilt production image, HIGH and CRITICAL, exit 1aquasecurity/[email protected] · timeout 20 minpassed
  • IaC misconfiguration scandockerfile, kubernetes and github_actions frameworks, offlinecheckov:3.3.13 · timeout 10 minpassed

Merge allowed

gitleaks read 119 commits and 67.61 MB with no leaks. Checkov passed 148 dockerfile checks with 2 skipped, 148 github_actions and 9 gitlab_ci.

The same three jobs run on GitLab CI against a self-hosted shell runner on g7. Pipeline 2783396207 went green in 209 s, created by the mirror push after a merge rather than by anyone pressing Run.

  • No path filters, deliberately. A gate that skips depending on which files a change touched is not a gate, so all three jobs run on every pull request and every push to main.
  • Every scanner is pinned by version, and the configuration scan runs with --skip-download so nothing about the repository is sent anywhere.
  • The allowlist holds 12 dated entries on this repository, each naming the finding, the date it was accepted and why the vulnerable path is unreachable in this build. An entry with no dated reason is not allowed.
  • This is supply-chain scanning in continuous integration. It is not an audit or a penetration test, and it says nothing about application logic or authentication.

Trigger and checkout

Entry

Pull request or push

Every PR and push to default, no path filters

Checkout

Full-depth clone

fetch-depth 0, so secrets stay reachable to the scan

Supply chain

Pinned scanners

Gitleaks, Trivy, and Checkov at named versions

Run · three jobs, four scans

The gates

Gate

Secret scan

Gitleaks over the whole history, redacted

Gate

Dependency scan

Trivy at HIGH and CRITICAL on pinned manifests

Gate

Image scan

Production image rebuilt in the job, then scanned

Gate

Config scan

Checkov on Dockerfiles, manifests, pipelines

Fail · exit 1 on HIGH or CRITICAL

Judgement and record

Exception record

Dated allowlists

Each exception names the finding, date, and reason

Ordering

Deferred failure

Records both exit codes, fails at the end

Evidence

Retained reports

Every job uploads its report, kept 30 days

Mirror · force-push after every merge

Second runner

Replication

GitLab mirror

Pushes the branch after each merge

Compute

Self-hosted shell runner

Tagged shell runner on a home server

Result

GitLab pipeline

Same jobs and thresholds, green in 209s

Text equivalent: A pull request or push to the default branch runs three jobs with no path filters. Gitleaks reads the full history, Trivy scans the dependency tree and a production image built in the job, and Checkov reads Dockerfiles, Kubernetes manifests, and the pipelines. A HIGH or CRITICAL finding exits non-zero; unfixable ones go into a dated allowlist. After each merge a mirror job pushes to GitLab, where the same jobs run on a self-hosted runner.

03

Engineering decisions

Fail the build, do not file a report

A finding at HIGH or CRITICAL exits non-zero and blocks the merge; clearing it by lowering the threshold would defeat the point.

Prove each gate by breaking it

Every gate got a pull request carrying the exact defect it catches; the config gate first passed an unhardened Dockerfile in a dot-directory and was fixed rather than recorded as a pass.

Scan the full history, never the diff

A key committed before these gates existed stays reachable in public history, so the scan reads the full clone, not the diff.

Rotation is the remediation

Rewriting history left matches fetchable under pull-request refs no force-push can reach, so rotating the key at the provider is what actually closed the exposure.

04

Verification evidence

  • Every gate was verified by a pull request that deliberately introduced its defect: a committed secret, a HIGH advisory, a vulnerable image, and a misconfiguration.
  • The secret gate found five credentials in the second repository's history where a hand-written disclosure had recorded two.
  • The GitLab pipeline ran green in 209 seconds on the self-hosted runner, created by the mirror push rather than a manual run.
verified-2026-08-23
$ gitleaks git . --redact --exit-code 1
full history scanned · no leaks
$ trivy filesystem --severity HIGH,CRITICAL
0 findings · 12 dated exceptions
✓ pipeline 2783396207 · source=push · 209s

05

Limitations