agentsclimarketplace

Container hardening

Skill roodlicht/accans-sec-skills/skills/container-hardening

Docker and OCI image hardening — base-image selection, USER/caps/read-only FS discipline, distroless migration, build-time scanning with trivy/grype, image signing via sigstore, and runtime guardrails (seccomp, AppArmor).From its SKILL.md

Install
npx -y skills add roodlicht/accans-sec-skills --skill container-hardening

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.0 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it

Container Hardening

When to use

This skill covers the container-image layer: what's inside, how it runs, and how you prove it checks out. It's the foundation k8s-security builds on (K8s takes these images and adds cluster-level controls).

Activates on:

  • A request like "review our Dockerfile", "migrate to distroless", "why does our container run as root", "trivy scan triage", "sign images with cosign".
  • A new or modified Dockerfile, Containerfile, docker-compose.yml, .dockerignore, multi-stage build script.
  • An image-scan output (trivy/grype/snyk container) that needs triaging.
  • A handoff from security-review phase 3 (container in scope) or from k8s-security (PodSecurityContext points to an image-level issue).
  • A supply-chain moment: image needs to be signed, attestation published. Together with supply-chain.

When NOT to use (handoff)

  • Kubernetes workload spec (PodSecurityContext, NetworkPolicy, RBAC) → k8s-security. The image is the ingredient; K8s is the cook.
  • SBOM format and signing-keys setup → supply-chain. This skill calls sigstore; that one explains it.
  • Vulnerabilities in packages inside the image → scanner output goes to cve-triage for triage.
  • Secrets in image layers → secrets-scanner on image history.
  • CI pipeline that runs the build → cicd-hardening.
  • Pure code question that just happens to run in a container → secure-coding or the framework skill.

Approach

Six phases. Phases 1–3 are image content, phase 4 is signing, phase 5 is runtime, phase 6 is verification.

1. Base-image choice

The base image is 80% of your attack surface. Choose deliberately.

Options, smallest to largest:

  • scratch (0 layers). Only for statically-linked binaries. Go, Rust, sometimes C. Smallest attack surface, no shell, no debug tools. Default for statically linked artifacts.
  • Distroless (Google). Minimal runtime for a language (Java, Python, Node), no shell, no package manager. gcr.io/distroless/java21-debian12, gcr.io/distroless/python3-debian12, etc. With a :debug variant for troubleshooting.
  • Chainguard Images / Wolfi. Glibc-free, continually rebuilt, SLSA-L3-signed. Comparable to distroless, with a demonstrably smaller CVE window thanks to daily builds.
  • Alpine (musl-libc). Small (~5 MB), package manager (apk) present. Caveat: musl can produce subtle differences vs glibc (DNS resolution, thread-locals).
  • Debian slim / Ubuntu minimal. Larger footprint but broader compatibility.
  • <language>:latest (e.g. node:latest, python:latest). Don't use. latest is unreproducible, and these tags carry far more than needed.

Rules:

  • Pin on digest, not on tag. FROM gcr.io/distroless/python3-debian12@sha256:<digest>. A tag can mutate under your feet.
  • Document update cadence. Base images get CVEs; you have to rebuild regularly. Renovate or Dependabot for Dockerfile dep bumps.
  • One image = one responsibility. Not one mega-image with app + migration + CLI tools; separate images with a shared base.

2. Dockerfile hygiene

The instructions that shape image content. Each of these is a classic foot-gun.

  • USER non-root. The default Docker user is root (UID 0). Explicit USER app or USER 10001 on a dedicated UID. Without USER: every process in the container is root in the container, and on container escape it's root on the host.
  • Read-only filesystem where possible. Via docker run --read-only or Kubernetes readOnlyRootFilesystem: true. Limit write needs to explicit tmpfs or volume mounts. Exposure: if an attacker gets execution, they can't drop malware.
  • Drop capabilities. Default Docker caps (NET_ADMIN, SYS_ADMIN selectively) are too broad. --cap-drop=ALL plus explicit additions for what you need (e.g. --cap-add=NET_BIND_SERVICE for ports < 1024). In Kubernetes: securityContext.capabilities.
  • Multi-stage builds. Build stage with compilers and dep installers, runtime stage minimal. Prevents gcc, make, git from ending up in the production image. See Docker docs for syntax.
  • .dockerignore. Prevents .env, .git, node_modules, test fixtures from landing in the image via COPY . .. Without .dockerignore your container has the whole source plus any local secrets.
  • No secrets in layers. ENV PASSWORD=..., ARG SECRET=..., or COPY .env . commits secrets into image history. Even after a later layer-delete the secret is still there. Use BuildKit secrets (RUN --mount=type=secret,id=...) or runtime injection via K8s secret / Vault.
  • Layer order for caching and security. Install dependencies first (rarely change), copy code later (frequently changes). Cache efficiency plus forced rebuild on dep change.
  • Apt/apk install cleanup. apt-get install --no-install-recommends plus rm -rf /var/lib/apt/lists/* in the same RUN. Otherwise package lists stay in the layer.
  • Add a HEALTHCHECK (when not via K8s liveness/readiness). Lets the orchestrator know the container is hung.
  • Document EXPOSE, don't publish. EXPOSE 8080 is documentation. Publishing happens at docker run -p or via a K8s Service.

Anti-pattern:

FROM node:latest                    # latest tag, unreproducible
COPY . /app                         # including .env, .git, tests
RUN npm install --unsafe-perm       # suggests root permission need
USER root                           # explicitly root
CMD ["npm", "start"]

Better variant:

FROM node:20-bookworm-slim@sha256:<digest> AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src

FROM gcr.io/distroless/nodejs20-debian12@sha256:<digest>
WORKDIR /app
COPY --from=build /app /app
USER 10001
CMD ["src/index.js"]

3. Build-time scanning

Before the image hits the registry:

  • Trivy (Aqua, Apache-2). Scans images for OS and app dep-vulns, secrets, misconfigs. Fastest in adoption.
    trivy image --severity HIGH,CRITICAL --exit-code 1 <image>:<tag>
    
  • Grype (Anchore, Apache-2). Vuln scanner combined with Syft for SBOM. Detailed per-package CVEs.
  • Clair (Red Hat OSS). Server-side scanning, integrates with Harbor.
  • Snyk container / Sysdig Secure (commercial). Enterprise options with broader feed sources and UI.
  • Docker Scout (Docker Inc). Default in Docker Desktop since 2023; comparable to Trivy for the use case.

Tie output to cve-triage. All high-severity findings face that skill's blocker criteria (KEV, reachable, exposed). Don't auto-block on every HIGH; you drown in low-impact base-image noise.

Scan configurations too: Trivy has --scanners misconfig for Dockerfile linting (overlapping with phase 2's rules).

4. Image signing and provenance

See supply-chain for the full explanation; here are the container-specific calls.

  • Sign at build (not after). Sigstore + cosign, keyless via OIDC.
    cosign sign --yes <registry>/<image>@sha256:<digest>
    
  • Attest the SBOM on the same image.
    syft <image>:<tag> -o cyclonedx-json > sbom.json
    cosign attest --yes --predicate sbom.json --type cyclonedx <image>@sha256:<digest>
    
  • SLSA provenance via slsa-framework/slsa-github-generator for GitHub Actions builds.
  • Verify on pull, not just on push. A Kubernetes admission controller (Kyverno, Sigstore policy-controller) that rejects unsigned images. Reference: k8s-security phase 1.

Registry choice:

  • Harbor (OSS) supports signing, vuln scanning, image replication.
  • Cloud registries (ECR, GAR, ACR) with built-in scanning.
  • GHCR for GitHub-attached builds.

5. Runtime hardening

The image is only half. Runtime policy decides what the container is allowed to do once it runs.

  • Seccomp — filter system calls. Docker's default profile is a good starting point; custom profiles for specific workloads (nginx, PostgreSQL) can be sourced from the moby/moby repo or via tools like dockerd-rootless-setuptool.
  • AppArmor / SELinux — mandatory access control. Default AppArmor profiles on Ubuntu/Debian, SELinux on RHEL/Fedora. Custom profiles via --security-opt apparmor=profile-name.
  • Rootless Docker / Podman — daemon runs as a non-root user; a container escape doesn't reach host-root. Default in Podman, opt-in in Docker.
  • No new privileges--security-opt=no-new-privileges:true prevents setuid escalation inside the container.
  • gVisor / Kata Containers — kernel isolation for higher trust levels. Overhead ~5–20%; pays off for multi-tenant workloads or sensitive data.

Ephemeral runtime: containers are stateless. State in volumes, not in writable layers. --rm for one-off runs, emptyDir or persistent-volume in K8s.

6. Verification-loop

Layer 1: scope (every Dockerfile in the repo covered? base images pinned by digest? USER non-root in every stage?), assumptions ("distroless so safe" only with the concrete image hash verified), gaps (both build-time and runtime controls present?), consistency (trivy severity threshold consistent across images?).

Layer 2: image-tag and digest syntax correct, CVE references in scan triage verified against NVD, SLSA-level claim backed by the concrete builder config, no fabricated seccomp-profile names.

Output

Container hardening review — <image(s)>
Dockerfiles in scope: <list>
Base images: <used, pinned by digest: yes/no>

Image content:
  USER non-root:            <yes/no per image>
  Multi-stage build:        <yes/no>
  Read-only compatible:     <yes/no, and which write paths>
  .dockerignore present:    <yes/no>
  Secrets in layers:        <none | found: ...>

Scan output (trivy/grype):
  Blockers (HIGH/CRITICAL): <N, with cve-triage handoff>
  Medium:                   <N>
  Misconfigs:               <list>

Signing + provenance:
  Signed:                   <yes/no, cosign identity>
  SBOM attested:            <yes/no, format>
  SLSA level effective:     <L0–L3>

Runtime policy:
  Seccomp:                  <default | custom | none>
  AppArmor/SELinux:         <profile | none>
  Capabilities dropped:     <ALL + adds | default>
  Rootless runtime:         <yes/no>

Findings (severity-sorted)

Verification-loop: ...

References

Categories

  • appsec

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.