Container Security Hardening Cheat Sheet
Dockerfile hardening, image scanning, runtime restrictions, and Kubernetes security contexts to reduce container attack surface.
Hardened Dockerfile
Non-root user, pinned base image, minimal surface, no build tools in final image.
FROM node:20.15-slim AS buildWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY . .RUN npm run buildFROM gcr.io/distroless/nodejs20-debian12WORKDIR /appCOPY --from=build /app/dist ./distCOPY --from=build /app/node_modules ./node_modulesUSER 1000:1000EXPOSE 8080ENTRYPOINT ["dist/server.js"]
Scan Images in CI
Fail the build on high/critical CVEs using Trivy.
trivy image --severity HIGH,CRITICAL --exit-code 1 \ --ignore-unfixed my-registry/my-app:${GIT_SHA}# Scan the Dockerfile itself for misconfigurationstrivy config .
Kubernetes Security Context
Pod-level restrictions that block common privilege-escalation paths.
apiVersion: v1kind: Podmetadata: name: webspec: securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 2000 seccompProfile: type: RuntimeDefault containers: - name: web image: my-registry/my-app:1.4.0 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"]
Hardening Checklist
The highest-leverage controls across build, image, and runtime.
- Pin base images- use digests or exact tags, never `latest`
- Multi-stage builds- keep compilers/build tools out of the runtime image
- Non-root user- set USER in the Dockerfile and runAsNonRoot in Kubernetes
- Read-only root FS- mount writable paths explicitly via emptyDir/volumes only
- Drop capabilities- `drop: ["ALL"]` and add back only what's required
- Signed images- sign with cosign and verify at admission (e.g. Kyverno/Sigstore policy)
- Network policies- default-deny, allow only required pod-to-pod traffic
Sign & Verify Images with Cosign
Attach a keyless (OIDC-backed) signature to an image and verify its provenance before deploy.
# Sign using CI's OIDC identity - no long-lived signing key to leakcosign sign --yes my-registry/my-app@sha256:abcd1234...# Verify the signature AND that it came from the expected workflowcosign verify \ --certificate-identity "https://github.com/org/repo/.github/workflows/deploy.yml@refs/heads/main" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ my-registry/my-app@sha256:abcd1234...
Admission Policy: Require Signed Images
Block any pod whose image isn't signed by the expected CI identity from ever scheduling.
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-signed-imagesspec: validationFailureAction: Enforce rules: - name: verify-signature match: resources: kinds: ["Pod"] verifyImages: - imageReferences: ["my-registry/*"] attestors: - entries: - keyless: subject: "https://github.com/org/repo/.github/workflows/deploy.yml@refs/heads/main" issuer: "https://token.actions.githubusercontent.com"
Default-Deny NetworkPolicy
Block all pod-to-pod traffic by default, then allow only the specific paths a service needs.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-all namespace: prodspec: podSelector: {} policyTypes: ["Ingress", "Egress"]---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-web-to-api namespace: prodspec: podSelector: matchLabels: { app: api } ingress: - from: - podSelector: { matchLabels: { app: web } } ports: - port: 8080
Runtime Detection with a Falco Rule
Catch what build-time scanning can't - anomalous behavior in an already-running container.
- rule: Unexpected shell in container desc: Detect a shell spawned inside a running container condition: > spawned_process and container and proc.name in (bash, sh, zsh) and not container.image.repository in (allowed_shell_images) output: > Shell spawned in container (user=%user.name container=%container.name image=%container.image.repository cmdline=%proc.cmdline) priority: WARNING
Hardening & Supply-Chain Glossary
Controls that sit above single-container Dockerfile hardening.
- Pod Security Standards- Privileged, Baseline, Restricted - Kubernetes' built-in namespace-level enforcement tiers
- SBOM- Software Bill of Materials; generate with `syft` so you know every package in an image
- Admission control- Kyverno/OPA Gatekeeper policies that reject non-compliant manifests at the API server
- External secrets- sync secrets from Vault/AWS Secrets Manager into cluster Secret objects, never bake into images
- Image provenance- SLSA attestations proving which pipeline/commit produced a given digest
- Runtime detection- eBPF-based tools (Falco, Tetragon) that flag anomalous syscalls in live containers
Scan both the image AND the running cluster — a clean image at build time can still be vulnerable a week later when a new CVE is disclosed for a package already baked in, so pair CI scanning with a runtime scanner like Trivy Operator or an admission-controller policy that blocks known-bad digests.