Skip to content
Back to skills

Container Hardening

BSecurity

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).

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 28, 2026
securitypythonrustgojavashellbashsqlnodenodejsrails

Works with

  • cli

Security analysis

B81/100
  • highPerforms destructive filesystem operations
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned May 28, 2026

npx -y skills add roodlicht/accans-sec-skills --skill container-hardening --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Container Hardening?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Container Hardening
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/roodlicht-container-hardening/badge)](https://www.skillsdirectory.com/skills/roodlicht-container-hardening)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: container-hardening
description: 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).
---

# 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**:

```dockerfile
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**:

```dockerfile
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.
  ```bash
  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.
  ```bash
  cosign sign --yes <registry>/<image>@sha256:<digest>
  ```
- **Attest the SBOM** on the same image.
  ```bash
  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

- Docker security docs — [https://docs.docker.com/engine/security/](https://docs.docker.com/engine/security/).
- OCI Image Spec — [https://github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec). Primary image-format spec.
- NIST SP 800-190 — [https://csrc.nist.gov/pubs/sp/800/190/final](https://csrc.nist.gov/pubs/sp/800/190/final). Application Container Security Guide.
- CIS Docker Benchmark — [https://www.cisecurity.org/benchmark/docker](https://www.cisecurity.org/benchmark/docker). Checklist form, good for audits.
- Docker Bench for Security — [https://github.com/docker/docker-bench-security](https://github.com/docker/docker-bench-security). Scripted CIS-Docker check.
- Distroless images — [https://github.com/GoogleContainerTools/distroless](https://github.com/GoogleContainerTools/distroless).
- Chainguard / Wolfi — [https://edu.chainguard.dev/chainguard/chainguard-images/](https://edu.chainguard.dev/chainguard/chainguard-images/). Continually rebuilt minimal images.
- Trivy — [https://trivy.dev/](https://trivy.dev/). Multi-purpose scanner.
- Grype + Syft — [https://github.com/anchore/grype](https://github.com/anchore/grype). Vuln scanner plus SBOM.
- Sigstore cosign — [https://docs.sigstore.dev/cosign/overview/](https://docs.sigstore.dev/cosign/overview/). Image signing without long-lived keys.
- Seccomp profiles — [https://docs.docker.com/engine/security/seccomp/](https://docs.docker.com/engine/security/seccomp/).
- OWASP Docker Security Cheat Sheet — [https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html).

## Categories

- appsec

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.