A production-ready DevSecOps project demonstrating how to integrate static security analysis, container image scanning, and compliance enforcement directly into a Kubernetes CI/CD pipeline β using Kubesec, KubeLinter, and Trivy, automated via GitHub Actions.
This repository demonstrates shifting security left β catching misconfigurations and vulnerabilities at the manifest and image level before anything reaches production Kubernetes. It answers the question:
"How do I make security automatic and enforced, not manual and optional?"
| Activity | Tool | Result |
|---|---|---|
| Kubernetes manifest scoring | Kubesec | secure-app scored +8, vulnerable-app scored -37 |
| Manifest policy linting | KubeLinter | 13 violations caught in vulnerable manifest |
| Container image CVE scanning | Trivy | Custom app image: 0 CRITICAL, 0 HIGH |
| CI/CD security gate | GitHub Actions | Blocks insecure code from merging automatically |
| Registry policy enforcement | OPA Gatekeeper | Restricts images to trusted registries only |
k8s-cicd-security-scanning/
βββ app/ # Hardened Node.js demo application
β βββ Dockerfile # Multi-stage build with apk upgrade + non-root user
β βββ .dockerignore
β βββ package.json
β βββ package-lock.json
β βββ src/
β βββ server.js # Express server with /health and / endpoints
β
βββ manifests/ # Kubernetes YAML files
β βββ secure-app.yaml # Hardened deployment (best practices applied)
β βββ network-policy.yaml # Default-deny NetworkPolicy
β βββ demo/
β βββ vulnerable-app.yaml # Intentionally insecure deployment (reference only)
β
βββ policies/ # Security policies
β βββ kubelinter-config.yaml # Custom KubeLinter rules
β βββ allowed-registries.yaml # Trusted image registries ConfigMap
β βββ image-policy-template.yaml # OPA Gatekeeper ConstraintTemplate
β βββ image-policy-constraint.yaml # OPA Gatekeeper Constraint
β
βββ scripts/ # Automation scripts
β βββ ci-cd-pipeline.sh # Full local pipeline simulation
β βββ kubesec-scan.sh # Kubesec batch scanner
β βββ kubelinter-scan.sh # KubeLinter batch scanner
β βββ image-scan.sh # Trivy image scanner
β
βββ reports/ # Scan output (generated at runtime, gitignored)
βββ .github/
β βββ workflows/
β βββ security-scan.yml # GitHub Actions CI/CD pipeline
βββ .gitignore
βββ README.md
Two deployments are included side by side for comparison:
manifests/demo/vulnerable-app.yaml β what NOT to do:
- Runs as root (
runAsUser: 0) - Privileged container (
privileged: true) - Allows privilege escalation
- Hardcoded secrets in environment variables
- No resource limits defined
- Outdated image (
nginx:1.14β hundreds of CVEs) - Kubesec score: -37 β
manifests/secure-app.yaml β what to do:
- Non-root user (
runAsUser: 1000) - Read-only root filesystem
- All Linux capabilities dropped (
drop: ALL) - Secrets from Kubernetes Secrets object
- CPU and memory requests/limits defined
- Liveness and readiness probes configured
- Minimal up-to-date image
- Kubesec score: +8 β
Kubesec assigns a numeric security score to Kubernetes manifests. A negative score means critical security issues are present. The pipeline fails on any negative score.
vulnerable-app β score: -37 β (pipeline blocked)
secure-app β score: +8 β
(pipeline passed)
KubeLinter checks for violations including missing probes, missing resource limits, running as root, writable root filesystems, host network access, and more β configured via policies/kubelinter-config.yaml.
Trivy scans every layer of the container image against the CVE database. The custom app image achieves zero HIGH or CRITICAL vulnerabilities by:
- Using
node:20-alpineas the base image - Running
apk upgrade --no-cachein both build and production stages - Using a multi-stage build to exclude build tools from the final image
- Excluding npm internal modules from the scanning scope
secure-demo-app:1.0.0
alpine packages β 0 vulnerabilities β
app dependencies β 0 vulnerabilities β
The CI/CD pipeline enforces hard rules on every push and pull request:
| Condition | Action |
|---|---|
| Kubesec score < 0 | β Pipeline fails, merge blocked |
| CRITICAL CVEs > 0 | β Pipeline fails, merge blocked |
| HIGH CVEs > 10 | β Pipeline fails, merge blocked |
| KubeLinter violations |
| Tool | Version | Install |
|---|---|---|
| Docker | 20.x+ | docs.docker.com |
| Minikube | 1.30+ | minikube.sigs.k8s.io |
| kubectl | 1.26+ | kubernetes.io/docs |
| Kubesec | v2.13.0 | See setup below |
| KubeLinter | v0.6.8 | See setup below |
| Trivy | latest | See setup below |
| jq | any | sudo apt-get install jq |
git clone https://github.com/muhammadhammad2005/k8s-cicd-security-scanning.git
cd k8s-cicd-security-scanningminikube start
kubectl get nodes# Kubesec (pinned version for stability)
curl -sSL https://github.com/controlplaneio/kubesec/releases/download/v2.13.0/kubesec_linux_amd64.tar.gz \
| tar xz kubesec
chmod +x kubesec && sudo mv kubesec /usr/local/bin/
kubesec version
# KubeLinter
curl -sSL https://github.com/stackrox/kube-linter/releases/download/v0.6.8/kube-linter-linux.tar.gz \
| tar xz
sudo mv kube-linter /usr/local/bin/
kube-linter version
# Trivy
sudo apt-get install wget apt-transport-https gnupg lsb-release -y
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" \
| sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy -y
trivy --version
# jq (required for JSON parsing in scripts)
sudo apt-get install jq -ychmod +x scripts/*.sh
./scripts/ci-cd-pipeline.shReports are saved to reports/.
# Scan secure manifest
kubesec scan manifests/secure-app.yaml
# Scan vulnerable manifest (demo reference)
kubesec scan manifests/demo/vulnerable-app.yaml
# Batch scan all production manifests
./scripts/kubesec-scan.sh# Quick lint
kube-linter lint manifests/
# Full scan with reports saved in all formats
./scripts/kubelinter-scan.sh# Scan the custom app image (full)
trivy image secure-demo-app:1.0.0
# Show only HIGH and CRITICAL, exclude npm internals
trivy image \
--skip-dirs /usr/local/lib/node_modules/npm \
--skip-dirs /opt/yarn-v1.22.22 \
--severity HIGH,CRITICAL \
secure-demo-app:1.0.0# Point Docker to Minikube's daemon (no push to registry needed)
eval $(minikube docker-env)
# Build the hardened image
docker build -t secure-demo-app:1.0.0 ./app
# Verify image exists
docker images | grep secure-demo-apptrivy image \
--skip-dirs /usr/local/lib/node_modules/npm \
--skip-dirs /opt/yarn-v1.22.22 \
--severity HIGH,CRITICAL \
secure-demo-app:1.0.0
# Expected result: 0 CRITICAL, 0 HIGHkubectl apply -f manifests/secure-app.yaml
kubectl apply -f manifests/network-policy.yaml
# Watch pods come up (takes ~35s for probes to pass)
kubectl get pods -w# Terminal 1 - forward the port
kubectl port-forward svc/secure-app-service 8080:80
# Terminal 2 - test endpoints
curl http://localhost:8080
# {"app":"secure-demo-app","version":"1.0.0","message":"Running securely in Kubernetes"}
curl http://localhost:8080/health
# {"status":"ok","uptime":12.345}kubectl delete -f manifests/secure-app.yaml
kubectl delete -f manifests/network-policy.yaml
eval $(minikube docker-env -u)After running scans, check the reports/ directory:
| Report file | Tool | What it shows |
|---|---|---|
kubesec-secure-app.json |
Kubesec | Score +8, passed checks breakdown |
kubesec-vulnerable-app.json |
Kubesec | Score -37, critical failures |
kubelinter-full.json |
KubeLinter | All violations with object names |
kubelinter-summary.txt |
KubeLinter | Human-readable summary |
trivy-secure-demo-app.json |
Trivy | CVE scan of custom app image |
echo "Secure app score:"
cat reports/kubesec-secure-app.json | \
jq '[.[] | select(.object | startswith("Deployment"))] | .[0].score'
echo "Vulnerable app score:"
cat reports/kubesec-vulnerable-app.json | \
jq '[.[] | select(.object | startswith("Deployment"))] | .[0].score'echo "CRITICAL CVEs:"
cat reports/trivy-secure-demo-app.json | \
jq '[.Results[]?.Vulnerabilities // [] | .[] | select(.Severity=="CRITICAL")] | length'
echo "HIGH CVEs:"
cat reports/trivy-secure-demo-app.json | \
jq '[.Results[]?.Vulnerabilities // [] | .[] | select(.Severity=="HIGH")] | length'The pipeline at .github/workflows/security-scan.yml triggers automatically on every push to main or develop, and on every pull request to main.
Push / PR to main
β
βΌ
βββββββββββββββββββββββββββ
β Install Tools β Kubesec v2.13.0, KubeLinter v0.6.8, Trivy latest
ββββββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Kubesec Analysis β Score manifests/secure-app.yaml
β β score < 0 β FAIL β
ββββββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β KubeLinter Analysis β Lint all manifests/
β β violations β WARNING β οΈ
ββββββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Build App Image β docker build secure-demo-app:1.0.0
ββββββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Trivy Image Scan β Scan secure-demo-app:1.0.0
β β CRITICAL > 0 or HIGH > 10 β FAIL β
ββββββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Upload Reports β Saved as artifact (30 days retention)
ββββββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Security Gate Summary β Final results printed
ββββββββββββββ¬βββββββββββββ
β
βΌ
PASS β
Kubesec | kubesec-secure-app | score: 8 β
Trivy | trivy-secure-demo-app | CRITICAL: 0 HIGH: 0 β
- Fork or clone this repo
- Push to your GitHub account
- Go to Actions tab β pipeline runs automatically on push
- Download security reports from the Artifacts section after each run
- No secrets or tokens required β tools are installed fresh each run
The policies/ directory includes OPA Gatekeeper templates to enforce image registry restrictions at the Kubernetes admission controller level β Kubernetes itself rejects deployments from untrusted registries before they even run.
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.13.0/deploy/gatekeeper.yaml
# Wait for it to be ready
kubectl -n gatekeeper-system wait --for=condition=ready pod --all --timeout=120skubectl apply -f policies/image-policy-template.yaml
kubectl apply -f policies/image-policy-constraint.yaml# Should be REJECTED (untrusted registry)
kubectl run test --image=untrusted-registry.com/myapp:latest
# Should be ALLOWED (trusted registry)
kubectl run test --image=docker.io/nginx:1.21-alpine- β
Non-root user (
runAsUser: 1000) - β Read-only root filesystem
- β
All Linux capabilities dropped (
drop: ALL) - β No privilege escalation allowed
- β Multi-stage Docker build
- β
Minimal Alpine base image (
node:20-alpine) - β
apk upgrade --no-cachepatches OS CVEs at build time - β Zero HIGH/CRITICAL CVEs in final image
- β Resource requests and limits on all containers
- β Liveness and readiness probes configured
- β Secrets from Kubernetes Secrets, not hardcoded env vars
- β Default-deny NetworkPolicy
- β ClusterIP service only (not exposed externally)
- β Trusted image registry enforcement via OPA Gatekeeper
- β Automated manifest scoring on every push
- β Automated image CVE scanning on every push
- β Security gate blocking bad code from merging
- β Reports uploaded as downloadable artifacts (30 days)
| Issue | Cause | Fix |
|---|---|---|
Pod ImagePullBackOff |
Image not in Minikube | Run eval $(minikube docker-env) before building |
Pod CrashLoopBackOff |
App error | Run kubectl logs -l app=secure-app |
| Probes failing on port 8080 | Port mismatch | App listens on 3000 β ensure manifest uses port 3000 |
npm ci fails in Docker |
Missing package-lock.json |
Run npm install in app/ directory first |
| Kubesec exit code 2 | Unsupported resource type (Secret/Service) | Use || true and filter score by Deployment in jq |
| Trivy finds npm CVEs | npm internal packages bundled in Node image | Add --skip-dirs /usr/local/lib/node_modules/npm |
GitHub Actions sed fails |
| delimiter conflict |
Use # as sed delimiter instead of / or | |
actions/upload-artifact fails |
Deprecated v3 | Upgrade to actions/upload-artifact@v4 |
| Tool | Docs | Purpose |
|---|---|---|
| Kubesec | https://kubesec.io | Kubernetes manifest security scoring |
| KubeLinter | https://docs.kubelinter.io | Manifest policy linting |
| Trivy | https://aquasecurity.github.io/trivy | Container image CVE scanning |
| OPA Gatekeeper | https://open-policy-agent.github.io/gatekeeper | Admission controller policies |
| Minikube | https://minikube.sigs.k8s.io | Local Kubernetes cluster |
- Kubernetes cluster hardening & manifest security
- DevSecOps CI/CD pipelines
- Container image vulnerability scanning
- OPA Gatekeeper policy enforcement
- Automation using GitHub Actions & Bash
MIT β free to use, modify, and distribute.