performing-container-image-hardening skill (Anthropic-Cybersecurity-Skills)

From Public Agent Wiki

What it does. 'Harden container images by minimizing attack surface, stripping unnecessary Part of mukul975/Anthropic-Cybersecurity-Skills (817 security skills) (mukul975/Anthropic-Cybersecurity-Skills).

Upstream mukul975/Anthropic-Cybersecurity-Skills
Skill file skills/performing-container-image-hardening/SKILL.md
License Apache-2.0 (skill folder LICENSE)
Author mukul975
Fetched 2026-09-10

Install

  • npx skills add mukul975/Anthropic-Cybersecurity-Skills --skill performing-container-image-hardening, or copy the skill folder into ~/.claude/skills/performing-container-image-hardening/.
  • Raw file: curl -sL https://raw.githubusercontent.com/mukul975/Anthropic-Cybersecurity-Skills/HEAD/skills/performing-container-image-hardening/SKILL.md

SKILL.md (verbatim)

name: performing-container-image-hardening
description: 'Harden container images by minimizing attack surface, stripping unnecessary
  packages, implementing multi-stage builds, configuring non-root users, and applying
  CIS Docker Benchmark recommendations to produce secure, production-ready images.
  Use when building production container images, when compliance requires CIS Docker
  Benchmark adherence, or when shrinking image size to reduce vulnerability exposure
  from unused packages.

  '
domain: cybersecurity
subdomain: devsecops
tags:
- devsecops
- cicd
- container-hardening
- docker
- cis-benchmark
- secure-sdlc
version: 1.0.0
author: mahipal
license: Apache-2.0
nist_csf:
- PR.PS-01
- GV.SC-07
- ID.IM-04
- PR.PS-04
mitre_attack:
- T1195
- T1554
- T1059.004
- T1610
- T1611

Performing Container Image Hardening

When to Use

  • When building production container images that need minimal attack surface
  • When compliance requires CIS Docker Benchmark adherence for container configurations
  • When reducing image size to minimize vulnerability exposure from unused packages
  • When implementing defense-in-depth for containerized workloads
  • When migrating from fat base images to distroless or minimal images

Do not use for runtime container security monitoring (use Falco), for host-level Docker daemon hardening (use CIS Docker Benchmark host checks), or for container orchestration security (use Kubernetes security scanning).

Prerequisites

  • Docker or BuildKit for multi-stage builds
  • Base image options: distroless, Alpine, slim, or scratch
  • Container scanning tool (Trivy) for validation
  • CIS Docker Benchmark reference

Workflow

Step 1: Use Multi-Stage Builds to Minimize Image Size

# Build stage with all dependencies
FROM python:3.12-bookworm AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
COPY src/ ./src/
RUN python -m compileall src/

# Production stage with minimal base
FROM python:3.12-slim-bookworm AS production
RUN apt-get update && \
    apt-get install -y --no-install-recommends libpq5 && \
    rm -rf /var/lib/apt/lists/* && \
    apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false

COPY --from=builder /install /usr/local
COPY --from=builder /build/src /app/src

RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
RUN chown -R appuser:appuser /app

USER appuser
WORKDIR /app

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1

EXPOSE 8080
ENTRYPOINT ["python", "-m", "src.main"]

Step 2: Use Distroless Base Images

# Go application with distroless
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /server .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Step 3: Remove Unnecessary Components

# Hardened image checklist
FROM ubuntu:24.04 AS base

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      ca-certificates \
      libssl3 && \
    # Remove package manager to prevent runtime package installation
    apt-get purge -y --auto-remove apt dpkg && \
    rm -rf /var/lib/apt/lists/* \
           /var/cache/apt/* \
           /tmp/* \
           /var/tmp/* \
           /usr/share/doc/* \
           /usr/share/man/* \
           /usr/share/info/* \
           /root/.cache

# Remove shells if not needed
RUN rm -f /bin/sh /bin/bash /usr/bin/sh 2>/dev/null || true

# Remove setuid/setgid binaries
RUN find / -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true

Step 4: Configure Read-Only Filesystem

# Kubernetes deployment with read-only root filesystem
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hardened-app
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          image: app:hardened
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /app/cache
      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 100Mi
        - name: cache
          emptyDir:
            sizeLimit: 50Mi

Step 5: Pin Base Image by Digest

# Pin to exact image digest for reproducibility
FROM python:3.12-slim-bookworm@sha256:abcdef1234567890 AS production
# This ensures the exact same base image is used every time

Step 6: Validate Hardening with Automated Scanning

# Scan hardened image with Trivy
trivy image --severity HIGH,CRITICAL hardened-app:latest

# Check CIS Docker Benchmark compliance
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/docker-bench-security

# Verify no root processes
docker run --rm hardened-app:latest whoami
# Expected: appuser (NOT root)

# Verify read-only filesystem
docker run --rm hardened-app:latest touch /test 2>&1
# Expected: Read-only file system error

Key Concepts

Term Definition
Multi-Stage Build Docker build technique using multiple FROM stages to separate build and runtime, reducing final image size
Distroless Google-maintained minimal container images containing only the application and runtime dependencies
Non-Root User Running container processes as unprivileged user to limit impact of container escape exploits
Read-Only Root Mounting the container root filesystem as read-only to prevent runtime modification
Image Digest SHA256 hash uniquely identifying an exact image version, more precise than mutable tags
Scratch Image Empty Docker base image used for statically compiled binaries requiring no OS
Security Context Kubernetes pod/container-level security settings controlling privileges, filesystem, and capabilities

Tools & Systems

  • Docker BuildKit: Advanced Docker build engine supporting multi-stage builds and build secrets
  • Distroless Images: Google's minimal container base images (static, base, java, python, nodejs)
  • docker-bench-security: Script checking CIS Docker Benchmark compliance
  • Trivy: Container image vulnerability and misconfiguration scanner
  • Hadolint: Dockerfile linter enforcing best practices

Common Scenarios

Scenario: Reducing a 1.2GB Python Image to Under 150MB

Context: A data science team uses python:3.12 as base image (1.2GB) with scientific computing packages. The image has 200+ known CVEs from unnecessary system packages.

Approach:

  1. Switch to python:3.12-slim-bookworm as base (150MB) and install only required system libraries
  2. Use multi-stage build: compile C extensions in builder stage, copy wheels to production
  3. Pin numpy, pandas, and scipy to pre-built wheels to avoid build dependencies in production
  4. Remove pip, setuptools, and wheel from the final image
  5. Create non-root user and set filesystem permissions
  6. Validate with Trivy: expect CVE count to drop from 200+ to under 20

Pitfalls: Some Python packages require shared libraries at runtime (libgomp, libstdc++). Test the application thoroughly after removing system packages. Alpine-based images use musl libc which can cause compatibility issues with numpy and pandas.

Output Format

Container Image Hardening Report
==================================
Image: app:hardened
Base: python:3.12-slim-bookworm
Date: 2026-02-23

SIZE COMPARISON:
  Before hardening: 1,247 MB (python:3.12)
  After hardening:  143 MB  (python:3.12-slim + multi-stage)
  Reduction: 88.5%

SECURITY CHECKS:
  [PASS] Non-root user configured (appuser:1000)
  [PASS] HEALTHCHECK instruction present
  [PASS] No setuid/setgid binaries found
  [PASS] Package manager removed
  [PASS] Base image pinned by digest
  [PASS] No shell access (/bin/sh removed)
  [WARN] /tmp writable (emptyDir mounted)

VULNERABILITY COMPARISON:
  Before: 234 CVEs (12 Critical, 45 High)
  After:  18 CVEs (0 Critical, 3 High)
  Reduction: 92.3%

Other files in this skill

assets/template.md (verbatim)

Container Image Hardening Templates

Hardened Python Dockerfile

FROM python:3.12-slim-bookworm AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.12-slim-bookworm AS production
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && \
    rm -rf /var/lib/apt/lists/* && \
    groupadd -r appuser && useradd -r -g appuser -s /sbin/nologin appuser && \
    find / -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
COPY --from=builder /install /usr/local
COPY --chown=appuser:appuser src/ /app/src/
USER appuser
WORKDIR /app
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1
EXPOSE 8080
ENTRYPOINT ["python", "-m", "src.main"]

Hardened Go Dockerfile (Distroless)

FROM golang:1.22 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /server .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Hadolint Configuration

# .hadolint.yaml
ignored:
  - DL3008  # Pin versions in apt-get (use --no-install-recommends instead)
trustedRegistries:
  - docker.io
  - gcr.io
  - ghcr.io

references/api-reference.md (verbatim)

API Reference: Container Image Hardening Audit

Libraries Used

Library Purpose
subprocess Execute Trivy, Docker, and hadolint CLI commands
json Parse vulnerability scan and inspection results
re Analyze Dockerfile instructions
pathlib Handle Dockerfile and image paths

Installation

# Trivy vulnerability scanner
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Hadolint Dockerfile linter
wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint

# Docker CLI (already installed in most environments)

Trivy Image Scanning

Scan Image for Vulnerabilities

import subprocess
import json

def scan_image(image_name, severity="CRITICAL,HIGH"):
    cmd = [
        "trivy", "image",
        "--format", "json",
        "--severity", severity,
        "--exit-code", "0",
        image_name,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    return json.loads(result.stdout) if result.stdout else {}

Scan for Secrets in Image

def scan_secrets(image_name):
    cmd = [
        "trivy", "image",
        "--format", "json",
        "--scanners", "secret",
        image_name,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    return json.loads(result.stdout) if result.stdout else {}

Scan for Misconfigurations

def scan_misconfig(image_name):
    cmd = [
        "trivy", "image",
        "--format", "json",
        "--scanners", "misconfig",
        image_name,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    return json.loads(result.stdout) if result.stdout else {}

Docker Image Inspection

Inspect Image Metadata

def inspect_image(image_name):
    cmd = ["docker", "inspect", image_name]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    data = json.loads(result.stdout)[0]
    config = data.get("Config", {})
    return {
        "image": image_name,
        "user": config.get("User", "root"),
        "exposed_ports": list(config.get("ExposedPorts", {}).keys()),
        "env_vars": config.get("Env", []),
        "entrypoint": config.get("Entrypoint"),
        "cmd": config.get("Cmd"),
        "labels": config.get("Labels", {}),
        "layers": len(data.get("RootFS", {}).get("Layers", [])),
        "size_mb": round(data.get("Size", 0) / 1048576, 1),
    }

Check for Root User

def check_non_root(image_name):
    inspection = inspect_image(image_name)
    user = inspection["user"]
    return {
        "image": image_name,
        "runs_as_root": user in ("", "root", "0"),
        "user": user or "root (default)",
        "severity": "high" if user in ("", "root", "0") else "pass",
    }

Hadolint Dockerfile Linting

def lint_dockerfile(dockerfile_path):
    cmd = [
        "hadolint",
        "--format", "json",
        str(dockerfile_path),
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    findings = json.loads(result.stdout) if result.stdout else []
    return [
        {
            "line": f["line"],
            "code": f["code"],
            "level": f["level"],
            "message": f["message"],
        }
        for f in findings
    ]

Hardening Checks

Common Dockerfile Issues

def audit_dockerfile(dockerfile_path):
    findings = []
    with open(dockerfile_path) as f:
        lines = f.readlines()

    has_user = False
    has_healthcheck = False

    for i, line in enumerate(lines, 1):
        stripped = line.strip()
        if stripped.startswith("USER") and stripped.split()[-1] not in ("root", "0"):
            has_user = True
        if stripped.startswith("HEALTHCHECK"):
            has_healthcheck = True
        if stripped.startswith("FROM") and ":latest" in stripped:
            findings.append({
                "line": i, "severity": "medium",
                "issue": "Using :latest tag — pin specific version",
            })
        if "ADD" in stripped and ("http://" in stripped or "https://" in stripped):
            findings.append({
                "line": i, "severity": "high",
                "issue": "ADD with remote URL — use COPY + curl for verification",
            })

    if not has_user:
        findings.append({"line": 0, "severity": "high", "issue": "No USER instruction — runs as root"})
    if not has_healthcheck:
        findings.append({"line": 0, "severity": "low", "issue": "No HEALTHCHECK instruction"})

    return findings

Output Format

{
  "image": "myapp:v1.2.3",
  "vulnerabilities": {
    "critical": 2,
    "high": 8,
    "medium": 15,
    "low": 23
  },
  "runs_as_root": false,
  "size_mb": 142.5,
  "layers": 12,
  "dockerfile_issues": 3,
  "secrets_found": 0,
  "findings": [
    {
      "type": "vulnerability",
      "package": "openssl",
      "installed": "3.0.2",
      "fixed": "3.0.13",
      "severity": "CRITICAL",
      "cve": "CVE-2024-0727"
    }
  ]
}

references/standards.md (verbatim)

Standards Reference: Container Image Hardening

CIS Docker Benchmark v1.6.0 - Image Controls

  • 4.1: Create a user for the container
  • 4.2: Use trusted base images
  • 4.3: Do not install unnecessary packages
  • 4.4: Scan and rebuild images for security patches
  • 4.6: Add HEALTHCHECK instruction
  • 4.7: Do not use update instructions alone
  • 4.9: Use COPY instead of ADD
  • 4.10: Do not store secrets in Dockerfiles
  • 4.11: Install verified packages only

NIST SP 800-190 Application Container Security Guide

Image Hardening (Section 4.1)

  • Use minimal base images to reduce attack surface
  • Remove unnecessary tools, shells, and package managers
  • Scan images for vulnerabilities before deployment
  • Sign images for integrity verification

OWASP Docker Security Cheat Sheet

  • Use multi-stage builds
  • Run as non-root user
  • Use read-only root filesystem
  • Pin base images to digests
  • Drop all Linux capabilities
  • Enable seccomp profile

references/workflows.md (verbatim)

Workflow Reference: Container Image Hardening

Hardening Pipeline

Base Image Selection
       │
       ▼
┌──────────────────┐
│ Multi-Stage Build │
│ (builder + prod)  │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│ Remove Packages  │
│ + Set User/Perms │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│ Trivy Scan       │
│ + Hadolint       │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│ CIS Benchmark    │
│ Validation       │
└──────────────────┘

Base Image Selection Guide

Base Image Size Use Case Packages
scratch 0 MB Static Go/Rust binaries None
distroless/static 2 MB Static binaries + CA certs ca-certificates
distroless/base 20 MB Dynamic binaries glibc, libssl
alpine:3.19 7 MB General minimal musl, busybox
debian:bookworm-slim 80 MB Debian ecosystem apt, glibc
ubuntu:24.04 78 MB Ubuntu ecosystem apt, glibc

Dockerfile Hardening Checklist

  • Multi-stage build separates build and runtime
  • Minimal base image selected
  • Non-root USER instruction present
  • HEALTHCHECK instruction defined
  • Base image pinned by digest
  • COPY used instead of ADD
  • No secrets in ENV or ARG
  • Package cache cleaned (rm -rf /var/lib/apt/lists/*)
  • Unnecessary packages removed
  • setuid/setgid bits cleared
  • Shell removed (if not needed)

Back to mukul975/Anthropic-Cybersecurity-Skills (817 security skills) or Agent skills.