Skip to content
Back to skills

Container Security

FSecurity

Hardening for Dockerfiles, OCI images, Kubernetes manifests, and Helm charts. Use when generating a Dockerfile or image build, writing Kubernetes, Helm, or Kustomize manifests, or reviewing container changes in a pull request.

  • 22 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
securitypythonrustgoshellnodedockerkubernetesterraformdebugginggit

Works with

  • cli
  • api

Security analysis

F2/100
  • criticalPipes output to a shell interpreter
  • highPerforms destructive filesystem operations
  • criticalDownloads and executes remote scripts — classic supply chain attack
  • criticalDownloads and executes remote scripts — classic supply chain attack
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 5 files and shows the line behind each finding

Scanned September 6, 2026

npx -y skills add ShieldNet-360/secure-vibe --skill container-security --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Container Security?

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

Security grade badge for Container Security
[![Security: F — Skills Directory](https://www.skillsdirectory.com/api/skills/shieldnet-360-container-security-ea0688b8/badge)](https://www.skillsdirectory.com/skills/shieldnet-360-container-security-ea0688b8)

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
---
id: container-security
version: "1.6.0"
title: "Container Security"
description: "Hardening for Dockerfiles, OCI images, Kubernetes manifests, and Helm charts. Use when generating a Dockerfile or image build, writing Kubernetes, Helm, or Kustomize manifests, or reviewing container changes in a pull request."
category: hardening
severity: high
applies_to:
  - "when generating a Dockerfile or OCI image build"
  - "when generating Kubernetes / Helm / Kustomize manifests"
  - "when reviewing container changes in PR"
languages: ["dockerfile", "yaml", "go", "python"]
token_budget:
  minimal: 2400
  compact: 3100
  full: 4200
rules_path: "checklists/"
related_skills: ["iac-security", "secret-detection", "iam-best-practices", "supply-chain-security"]
last_updated: "2026-08-12"
sources:
  - "CIS Docker Benchmark v1.6"
  - "CIS Kubernetes Benchmark v1.9"
  - "NIST SP 800-190 Application Container Security Guide"
  - "OWASP Docker Top 10"
external_tools:
  - name: hadolint
    purpose: "Dockerfile linting (~60 ShellCheck-backed rules)"
    command: "hadolint <file>"
  - name: trivy
    purpose: "container image + filesystem CVE scan (scans installed packages, not just Dockerfile text)"
    command: "trivy image <image> | trivy fs ."
---

# Container Security

## Rules (for AI agents)

### ALWAYS
- Use **multi-stage builds**: separate builder/test stages from the final runtime
  image so build toolchains and source aren't shipped. The last stage should be a
  minimal base — `gcr.io/distroless/<variant>`, `scratch`, or a versioned `-slim` /
  `-alpine` variant — pinned by SHA256 digest, not just tag. Two bases have nothing
  to pin: `scratch`, and a reference to an earlier stage in the same file
  (`FROM base`), which is pinned on that stage's own `FROM` line.
  <!-- pattern: { id: dkr-multi-stage, severity: high, check: llm } -->
  <!-- pattern: { id: dkr-pinned-base-digest, severity: high, cwe: 1357, check: deterministic } -->
- Run as a non-root user: set `USER` explicitly on the **final stage**, as a **number**
  not a name. Omitting it leaves the container running as root. K8s `runAsNonRoot`
  rejects UID 0 and cannot resolve a username, so `USER appuser` fails at startup with
  "image has non-numeric user"; any non-zero UID passes, and 10000+ by convention also
  avoids colliding with host accounts.
  <!-- pattern: { id: dkr-missing-user-directive, severity: critical, cwe: 250, check: deterministic } -->
  <!-- pattern: { id: dkr-non-root-user, severity: critical, cwe: 250, check: deterministic } -->
- Use **`npm ci`** (and equivalents `pnpm install --frozen-lockfile`,
  `yarn install --frozen-lockfile`) in container builds, not `npm install`.
  `npm install` mutates the lockfile and resolves versions per-build,
  producing non-deterministic images that drift from the lockfile.
  <!-- pattern: { id: dkr-npm-install-not-ci, severity: medium, check: deterministic } -->
- Add a `.dockerignore` excluding `.git`, `node_modules`, `.env`, `*.pem`, `*.key`,
  `target/`, `.terraform/`, `dist/`, `coverage/`.
  <!-- pattern: { id: dkr-dockerignore-exists, severity: high, check: llm } -->
- Build with **BuildKit** (default since Docker Engine 23; `DOCKER_BUILDKIT=1` on
  older engines) so `RUN --mount=type=secret,id=<name>` is available for build-time
  credentials. `# syntax=docker/dockerfile:1` selects the frontend and does **not**
  enable BuildKit by itself.
  <!-- pattern: { id: dkr-build-with-buildkit, severity: low, check: llm } -->
- Emit an **SBOM** (`docker buildx --sbom=true` / `syft`) and attach it to the
  image so downstream scanners can audit the dependency set.
  <!-- pattern: { id: dkr-sbom-emitted, severity: medium, check: llm } -->
- Pin **apt** packages and clean lists in the same layer: `apt-get update &&
  apt-get install -y --no-install-recommends pkg=1.2.3 && rm -rf
  /var/lib/apt/lists/*`. The `update` must be in that same `RUN` — without it there
  are no package lists; in an earlier layer it goes stale behind the build cache.
  <!-- pattern: { id: dkr-apt-version-pin, severity: medium, cwe: 1357, check: deterministic } -->
- Set explicit `HEALTHCHECK` for long-running services, and separately set
  `livenessProbe` / `readinessProbe` / `startupProbe` in K8s. Kubernetes never runs
  the image's `HEALTHCHECK` — probes are the only mechanism there, so neither one
  covers the other.
  <!-- pattern: { id: dkr-healthcheck-defined, severity: medium, check: llm } -->
- Set resource `requests` and `limits` on every container (CPU and memory). Without a
  memory limit, one container can exhaust the node and take down every pod scheduled
  beside it — the limit is what bounds a runaway's blast radius to its own container.
- Harden at the **container-level** `securityContext`
  (`spec.containers[].securityContext`): `capabilities.drop: [ALL]` then add back only
  what's needed, `readOnlyRootFilesystem: true`, `allowPrivilegeEscalation: false`.
  These three exist **only** at container level — under the pod's `spec.securityContext`
  they are silently ignored while the manifest still applies, so the control vanishes
  with nothing to show for it. Use `emptyDir` for paths that must stay writable.
- Apply a seccomp profile (`seccompProfile.type: RuntimeDefault` at minimum) and
  AppArmor / SELinux where available. This one — like `runAsNonRoot` and
  `runAsUser` — may be set on the pod and inherited by its containers.
- Scan every image in CI (Trivy, Grype, Snyk, or your registry's scanner) and triage
  findings on severity, reachability and fix availability together. Fail the build on
  fixable CRITICAL / HIGH findings; a **material vulnerability with no published fix
  needs mitigation or a recorded risk acceptance, not an automatic pass** — "unfixable"
  is remediation information, not a reason it stopped mattering.
  <!-- pattern: { id: dkr-image-scan-required, severity: high, check: llm } -->
- Set `automountServiceAccountToken: false` on every workload that does not call the
  Kubernetes API. The default mounts a real ServiceAccount token into the container,
  so an RCE in an app that never needed cluster access still hands the attacker one.
- For **multi-tenant** workloads (per-user/per-customer sessions on shared
  infra), isolate tenants at the **kernel boundary**: a separate VM — or
  gVisor / Kata — per tenant, never just separate containers on one shared
  daemon. Drop `privileged`, enable user namespaces, and give each tenant its
  own network. A privileged container on a shared host escapes to the host
  trivially, so on shared infra that is full compromise of *every* co-tenant.
- Expose container orchestration to clients only through a **scoped,
  authenticated broker API** that performs the few operations a client may
  request (start/stop *my* session). The client must never hold direct daemon
  or cluster access.
- Consult `iam-best-practices` for cluster RBAC and the identity a workload runs as,
  and `supply-chain-security` for base-image provenance. This skill owns the
  container's own configuration, not what it inherits.

### NEVER
- Run containers as root or with `privileged: true` / `allowPrivilegeEscalation:
  true` outside of explicit, audited system pods (e.g., CNI plugins).
- Use a base image that is **past its vendor's end-of-life date**, or any
  non-LTS release of a runtime that ships an LTS line. EOL images stop
  receiving security patches, so a maintained image with open CVEs is
  still safer than an EOL one with none. Resolve the date against
  `endoflife.date/<runtime>` rather than from memory — a version that was
  supported when this rule was written may not be today. A dated snapshot
  of the common runtimes is in `references/eol-base-images.md`.
  <!-- pattern: { id: dkr-eol-base-image, severity: critical, cwe: 1104, check: llm } -->
- Mount the host docker socket (`/var/run/docker.sock`) inside an application
  container. It's effectively root on the host.
- Expose the container **daemon API over the network** (`tcp://…:2375`, or
  `:2376` even with TLS) to clients or apps. The daemon API is root-on-host:
  whoever reaches it runs arbitrary privileged containers and mounts the host
  filesystem. (A desktop/CLI app talking straight to a remote daemon is the
  same anti-pattern as a mounted `docker.sock`, just over TCP.)
- Ship a **single shared client credential** (one mTLS cert/key, token, or
  kubeconfig bundled into every copy of a distributed app) to reach that daemon
  or cluster. Every install holds the same key — trivially extracted from the
  app bundle — so it grants every user identical access and cannot be revoked
  per-user. Issue per-user / per-session, short-lived, scoped credentials.
- Run a tenant's container `privileged` on a host shared with other tenants, or
  attach tenant containers to a **shared external bridge network** — the first
  gives container-escape → co-tenant takeover, the second gives cross-tenant
  L3 reachability.
- Embed secrets in image layers via `ENV`, `ARG`, `COPY`, or by `echo`-ing them
  to a file. Even if `--squash`'d, BuildKit cache and registry layers leak.
  <!-- pattern: { id: dkr-no-secrets-in-env, severity: critical, cwe: 798, check: deterministic } -->
  <!-- pattern: { id: dkr-no-secrets-in-build-args, severity: critical, cwe: 798, check: llm } -->
  <!-- pattern: { id: dkr-no-secret-leak-in-layers, severity: critical, check: llm } -->
- Run `curl … | sh` or `wget -O- … | sh` in a `RUN` — piping an unverified
  remote script to a shell is arbitrary remote code at build time. Download,
  verify a pinned SHA-256, then execute.
  <!-- pattern: { id: dkr-no-curl-pipe-sh, severity: critical, cwe: 829, check: deterministic } -->
- Use an **unversioned** tag as the final image base — `latest`, `stable`, or a bare
  variant like `python:slim` / `node:alpine`. Builds become non-reproducible and
  quietly pick up CVEs. The *versioned* variants (`python:3.12-slim`,
  `node:20-alpine`) are the recommended minimal bases: it is the missing version that
  makes a tag mutable, not the variant name.
  <!-- pattern: { id: dkr-explicit-latest-tag, severity: high, check: deterministic } -->
- Use `ADD <url>` to fetch a remote resource **unverified**. Either
  `ADD --checksum=sha256:<hash> <url>` (Dockerfile frontend 1.6+), or `RUN curl
  --fail` plus an explicit checksum step, or vendor the artifact.
  <!-- pattern: { id: dkr-no-add-remote, severity: medium, check: deterministic } -->
- Use `hostNetwork: true`, `hostPID: true`, or `hostIPC: true` for application
  pods.
- Run pods in the `kube-system` namespace, or any namespace without a
  `NetworkPolicy` and PodSecurity admission policy.

### KNOWN FALSE POSITIVES
- A **builder stage** legitimately runs as root, installs a toolchain, and skips apt
  version pins — it is discarded and never shipped. These rules target the *final*
  stage.
- `FROM scratch` and `FROM <earlier-stage>` are not unpinned bases: neither can carry
  a tag, and the stage reference is pinned on its own `FROM` line.
- A base-image CVE with no published upstream fix should not hold the build red
  indefinitely — but route it to mitigation or a recorded exception rather than
  dropping it, and re-check when the base image next moves.
- `latest` in a `docker-compose.dev.yml` or a throwaway local build is not a
  production reproducibility problem.
- Operators that legitimately need cluster-admin access (kubelet, CSI drivers,
  CNI plugins) require elevated privileges; they belong in `kube-system` or a
  dedicated namespace with auditing, not in application namespaces.
- Bare-metal Kubernetes nodes sometimes legitimately disable `seccomp` for
  drivers that aren't compatible; document the exception.
- One-shot debugging pods (kubectl debug, ephemeral containers) intentionally
  bypass many of these controls; they should not be persisted as YAML in the
  repo.
- A remote Docker / K8s endpoint over mTLS (`:2376`) is acceptable for an
  **operator's own** CI / build farm where each operator holds a personal,
  revocable cert — the anti-pattern is shipping **one shared** cert inside a
  distributed end-user app.
- `privileged` or a shared bridge network within a **single trust domain** (one
  team's own microservices, or a sim stack on the developer's own machine) is
  lower-risk than the multi-tenant case; these rules target the shared-host,
  cross-tenant blast radius specifically.

## Context (for humans)

Containers leak two ways: image-layer leaks (secrets in `ENV`, build artifacts
left in the final image, vulnerable base CVEs) and runtime escapes (privileged
mode, docker.sock, host namespaces). NIST SP 800-190 frames these as **image
risks**, **registry risks**, **orchestrator risks**, and **runtime risks**.

AI assistants almost always generate Dockerfiles that work and ship — fast — but
they default to a single-stage `FROM node` / `FROM python` and `USER root`. This
skill is the counterweight; pair it with `iam-best-practices` for cluster
RBAC and `supply-chain-security` for image provenance beyond the pod.

A distinct, often-missed class is **remote-daemon and multi-tenancy** exposure.
Handing a client app direct daemon access (`tcp://host:2375` + a cert shipped in
the app bundle) makes every user root on the host; running multiple tenants'
privileged containers on one shared daemon with a shared network means one
tenant's escape compromises all of them. The container hardening flags
(`privileged`, host namespaces, capabilities) matter most precisely where the
blast radius is multi-tenant — isolate at the VM/kernel boundary, and never let
a client touch the daemon directly.

## References

- `references/verifying-findings.md` — confirm or refute a finding, then lock it
- `references/eol-base-images.md` — dated snapshot of EOL runtimes; check endoflife.date for the current answer
- `checklists/k8s_pod_security.yaml`
- The nine deterministic `dkr-*` checks live in the scanner
  (`internal/tools/library_scanners.go`), not in a checklist file — run them with
  `secure-vibe scan` or the `scan_dockerfile` tool.
- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker).
- [CIS Kubernetes Benchmark](https://www.cisecurity.org/benchmark/kubernetes).
- [NIST SP 800-190](https://csrc.nist.gov/publications/detail/sp/800-190/final).

Files in this skill

  • SKILL.md14 KB
  • checklists/k8s_pod_security.yaml2.5 KB
  • references/eol-base-images.md1.6 KB
  • references/verifying-findings.md3.9 KB
  • tests/corpus.json2.6 KB

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.