CI/CD and supply-chain security
Secure delivery pipeline
A small Python API delivered the DevSecOps way: every change is tested and scanned, the image is built once, signed and deployed by GitOps, and the cluster only runs images signed by this pipeline.
At a glance
- 6
- automatic security checks
- 0
- stored signing keys
- 1
- human approval for production
- < 3 min
- from merge to dev
The goal
Most attacks on software today do not break the app: they break the way it is built and shipped. A leaked token, a poisoned image or a manual change in production. The goal: a delivery system where every step is automatic, checked and traceable, and where production only accepts what the pipeline built and signed. The app is small on purpose; the value is the delivery system around it.
What it shows
- Continuous integration
- ruff (with bandit rules), pytest with a 90 % coverage gate, a smoke test of the real container.
- Shift-left security
- gitleaks for secrets, Semgrep for code, Trivy and kubeconform for the Dockerfile and the Kubernetes files.
- Supply chain
- Trivy image scan, Syft SBOM (CycloneDX), cosign keyless signing, SBOM attestation, SLSA build provenance.
- Delivery
- Kustomize overlays, the image pinned by digest, Argo CD syncs dev by itself, production through a reviewed pull request.
- Runtime security
- Pod Security "restricted", non-root, read-only file system, no capabilities, seccomp, default-deny network policy.
- Admission control
- Kyverno lets in only images signed by this repository, and only pods with limits and probes.
Architecture
One change, from the developer to the cluster. The pipeline never gets cluster credentials: Argo CD pulls from Git.
Step by step
Pull request
Lint, unit tests, secret scan, SAST and IaC checks run in parallel. The image is built, smoke-tested, scanned and gets an SBOM. Nothing is published yet.
Merge to main
The same checks run again. The image is pushed to GHCR by digest, signed with cosign (keyless, the GitHub identity) and gets an SBOM attestation and SLSA provenance.
Deploy to dev
The pipeline commits the new digest to the dev overlay. Argo CD sees the commit and syncs dev by itself.
Promote to production
The pipeline opens a pull request for the prod overlay with the same digest. A human reviews and merges it: that merge is the release. Rollback = git revert.
Admission
Before a pod starts, Kyverno checks the signature and the identity of the pipeline, and the limits and probes. Anything else is refused.
Proof from a real run
Run on the main branch on 2026-10-27, from GitHub Actions. Time per job:
- Lint and unit tests 12s
- Secret scan (gitleaks) 10s
- SAST (semgrep) 33s
- IaC and manifest checks 15s
- Build, scan, SBOM, sign 1m 53s
- Deploy to dev (GitOps commit) 4s
Open this run (opens in a new tab)
· deploy(dev): shop-api bf471203925f
Security controls
| Control | What it stops |
|---|---|
| gitleaks in pre-commit and CI | Passwords and tokens in Git. |
| Semgrep SAST | Injection, unsafe functions and weak crypto in the code. |
| Trivy image scan with a gate | Shipping known critical vulnerabilities. |
| Digest instead of tag | A different image behind the same tag. |
| cosign keyless + Kyverno | Images that the pipeline did not build and sign. |
| Pull-based GitOps | Stolen CI credentials that reach the cluster. |
| Restricted pods + default-deny network | Break-out and lateral movement from one hacked pod. |
| Actions pinned to commit SHAs | A changed third-party action in the pipeline. |
Try it yourself
make install test # Python 3.12+: lint, tests, 90 % coverage gate
make run # http://127.0.0.1:8080/docs
docker compose up --build # the hardened container: read-only, non-root, no capabilities
make kind-up # local Kubernetes with ingress, Kyverno and Argo CD
# check the signature of a published image (keyless, GitHub OIDC identity)
cosign verify ghcr.io/s0sta/secure-delivery-pipeline@sha256:… \
--certificate-identity-regexp 'https://github.com/s0sta/secure-delivery-pipeline/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comWhat I learned
- Fair gates get accepted: fail on new critical findings first, then raise the bar step by step.
- Signing only matters when something checks it: the policy in the cluster is the real control.
- A real CI run finds what local tests miss: a deleted tool release, a missing path, a repository setting.