Security No. 273

Container Security skill for Claude Code

Minimal images, non-root pods, scanned on every build. Claude fixes container risk at the source: it swaps bloated base images for pinned distroless or slim ones, moves build secrets to BuildKit mounts, runs as a numeric non-root user, and applies a restricted securityContext with dropped capabilities and a read-only root filesystem. Trivy and hadolint gate CI, images get signed, and every change is measured against a CVE baseline.

  • namecontainer-security
  • size209 lines · ≈2,050 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Hardens container images and their runtime — Trivy scanning of images, filesystems, and IaC with CI gates, minimal and distroless base images, non-root users, no secrets in layers, image signing, Docker run flags, and Kubernetes pod hardening with securityContext, Pod Security Admission, NetworkPolicies, and service account scoping. Use when scanning or fixing image CVEs, writing or reviewing a Dockerfile or Kubernetes manifest for security, choosing a base image, or responding to a container finding.

Try asking

  • Trivy found 212 vulnerabilities in our API image — get that number down
  • Harden this Kubernetes deployment so it passes the restricted Pod Security profile
  • Review our Dockerfile for security problems and rewrite it

Use it when

  • Image scans report many OS or language package CVEs
  • Writing or reviewing a Dockerfile or Kubernetes manifest for production
  • Enforcing Pod Security Admission or NetworkPolicies in a namespace
  • Adding image scanning and signing to CI

Skip it when

How to check the result

Trivy passes the high and critical gate or every remaining finding is documented with an expiry, the container runs with a non-zero UID, and a privileged test pod is rejected by the namespace.

The common mistake it prevents

Passing secrets through ARG or ENV during the build, which bakes them into image layers where anyone who can pull the image can read them with docker history.

~/.claude/skills/container-security/SKILL.md

--- name: container-security description: Hardens container images and their runtime — Trivy scanning of images, filesystems, and IaC with CI gates, minimal and distroless base… license: MIT ---

Done well means: images are small, built from a pinned minimal base, run as non-root with no shell when possible, contain no secrets, are scanned in CI with a gate on fixable high/critical findings, are signed, and run under a restricted pod profile with dropped capabilities, a read-only root filesystem, and default-deny networking.

Workflow

- [ ] 1. Scan the current image and Dockerfile; record the baseline
- [ ] 2. Fix the base image first (most CVEs come from it)
- [ ] 3. Harden the Dockerfile (multi-stage, non-root, no secrets, pinned)
- [ ] 4. Harden runtime (docker flags or Kubernetes securityContext)
- [ ] 5. Add CI gates: scan, lint, sign
- [ ] 6. Rescan and compare against the baseline

1. Scan

trivy image --severity HIGH,CRITICAL myapp:1.4.2            # OS + language packages
trivy image --ignore-unfixed --exit-code 1 --severity HIGH,CRITICAL myapp:1.4.2   # CI gate
trivy image --scanners vuln,secret myapp:1.4.2              # also look for baked-in secrets
trivy fs --scanners vuln,secret,misconfig .                 # repo: lockfiles, secrets, IaC
trivy config .                                              # Dockerfile, k8s YAML, Terraform, Helm
hadolint Dockerfile                                         # Dockerfile lint
docker history --no-trunc myapp:1.4.2                       # see what each layer did

Triage like dependency findings: is the package used at runtime, is there a fixed version, does the base image already have a patched tag? Record accepted findings in .trivyignore with a comment and expiry (CVE-2025-XXXX exp:2026-06-30).

2. Base image

NeedBase
Static Go/Rust binarygcr.io/distroless/static-debian12:nonroot or scratch (add CA certs and /etc/passwd user yourself)
JVM, Node, Python runtimeDistroless language images (gcr.io/distroless/nodejs22-debian12, .../python3-debian12, .../java21-debian12) or Chainguard images
Need a package manager / shell at runtimedebian:12-slim, python:3.13-slim, node:22-slim; or Alpine if musl is fine for your deps
Compliance requires a vendor OSUBI minimal / vendor-hardened images
  • Pin by digest for release builds: FROM node:22-slim@sha256:<digest>; let Renovate or Dependabot bump digests so you still get patches.
  • Rebuild regularly (weekly) even without code changes; base images get patched.
  • Distroless/Chainguard images have no shell. Debug with kubectl debug ephemeral containers or the :debug image variants, not by adding a shell to prod.

3. Dockerfile hardening

# syntax=docker/dockerfile:1
FROM node:22-slim@sha256:<digest> AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --omit=dev
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs22-debian12:nonroot
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=build --chown=nonroot:nonroot /app/dist ./dist
USER nonroot
EXPOSE 8080
CMD ["dist/server.js"]

Rules:

  • Multi-stage: compilers, package managers, and source stay in the build stage.
  • Non-root USER with a numeric UID ideally (Kubernetes runAsNonRoot can only verify numeric UIDs): USER 10001:10001 on non-distroless images after useradd.
  • No secrets in ARG, ENV, or copied files: they persist in layers and image history. Use BuildKit --mount=type=secret (docker build --secret id=npmrc,src=$HOME/.npmrc .).
  • .dockerignore excluding .git, .env*, node_modules, keys, test data.
  • COPY specific paths, not COPY . . into the final stage.
  • No curl | sh; verify checksums for downloaded binaries.
  • Prefer COPY --chmod/--chown over extra RUN chmod layers.
  • No sudo, no SSH server, no latest tags.

4. Runtime hardening

Docker / Compose:

docker run --read-only --tmpfs /tmp \
  --cap-drop=ALL --security-opt=no-new-privileges:true \
  --user 10001:10001 --pids-limit 256 --memory 512m \
  myapp:1.4.2

Never: --privileged, mounting /var/run/docker.sock into app containers (root on the host), --net=host or --pid=host without a specific need.

Kubernetes pod spec:

spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile: { type: RuntimeDefault }
  containers:
    - name: app
      image: registry.example.com/myapp@sha256:<digest>
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities: { drop: ["ALL"] }
      resources:
        requests: { cpu: 100m, memory: 128Mi }
        limits: { memory: 512Mi }
      volumeMounts:
        - { name: tmp, mountPath: /tmp }
  volumes:
    - name: tmp
      emptyDir: {}

Namespace-level enforcement with Pod Security Admission:

apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted

Roll out with warn and audit labels first; kubectl label --dry-run=server against a namespace reports which existing pods would violate restricted.

Network: default-deny per namespace, then allow specific flows (requires a CNI that enforces NetworkPolicy, e.g. Calico or Cilium):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: payments }
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]

Remember to allow DNS egress (UDP/TCP 53 to kube-dns) after default-deny.

RBAC and secrets:

  • One service account per workload; bind only the verbs and resources it needs. No cluster-admin for apps; no wildcard * verbs.
  • Mount a token only where the app calls the Kubernetes API.
  • Enable etcd encryption at rest; prefer External Secrets / CSI secret store drivers over long-lived static secrets.
  • Block metadata endpoint access from pods (cloud-specific: IMDSv2 hop limit 1 on AWS, workload identity on GKE/AKS).

5. CI gates

# GitHub Actions excerpt: pin third-party actions by full commit SHA in real workflows
- name: Build
  run: docker build -t myapp:${{ github.sha }} .
- name: Scan
  run: trivy image --ignore-unfixed --exit-code 1 --severity HIGH,CRITICAL myapp:${{ github.sha }}
- name: Lint Dockerfile
  run: hadolint Dockerfile

Sign and verify images with Sigstore cosign (keyless in CI via OIDC): cosign sign registry.example.com/myapp@sha256:<digest> and verify in admission (Kyverno or Sigstore policy-controller) so only signed images from your CI identity run. Generate an SBOM per image (trivy image --format cyclonedx --output sbom.json <ref> or syft <ref>), and attach it as an attestation if your registry supports it.

Gotchas

  • --ignore-unfixed hides CVEs with no patch; still review criticals among them for mitigations.
  • readOnlyRootFilesystem breaks apps writing to /tmp, caches, or PID files; mount emptyDir volumes for those paths instead of dropping the setting.
  • Alpine’s musl can change DNS and performance behavior for some runtimes (Python wheels, JVM); test before switching.
  • Image scanners read package databases. Binaries copied in without a package manager (curl-downloaded tools) may be invisible; scan those artifacts separately.
  • A non-root user with CAP_NET_BIND_SERVICE dropped cannot bind ports below 1024; listen on 8080 and map it.

Verification

  • trivy image --ignore-unfixed --severity HIGH,CRITICAL --exit-code 1 passes, or every remaining finding is in .trivyignore with a reason and expiry.
  • docker run --rm --entrypoint id myapp:tag (non-distroless) shows a non-zero UID; kubectl get pod -o jsonpath='{.spec.securityContext}' matches the hardened spec.
  • The namespace enforces restricted; a test pod with privileged: true is rejected.
  • Report: base image before/after, image size before/after, CVE counts before/after.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc