agentleFS
GoldenWing-360GoldenWing-360/claude-security-skills/docker-container-security/SKILL.md

docker-container-security

Skill16 starsChanged 6 months agoMITView on GitHub

Run containers with a defensive baseline that survives production. Covers non-root users, read-only filesystems, dropped Linux capabilities, secret mounts instead of build-time bake-in, image scanning with trivy, distroless and minimal base images, and the Docker-bypasses-UFW firewall pitfall. Invoke when adding Docker to a VPS with UFW, writing a new Dockerfile, or pushing an image to a public registry.

  • Reads credentials

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    After a base-image CVE that affects your stack

    line 16
  2. Distinctive:

    Every production Dockerfile should:

    line 56
  3. Distinctive:

    Run a scanner on every image before deploy.

    line 168
  4. Distinctive:

    Default Docker bridge network puts all containers on one flat L2 segment.

    line 191
  5. Very specific:

    Before deploying any docker-compose.yml to production, walk this:

    line 236

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Docker / Container Security

A pragmatic baseline for Docker on a single VPS or a small cluster. Covers the Dockerfile, the run-time configuration, and the host-side gotchas — particularly the UFW-bypass that catches most people once.

When to invoke

  • Installing Docker on a VPS that has UFW (read the UFW section first — Docker bypasses UFW by default)
  • Writing a new Dockerfile or docker-compose.yml for production
  • Pushing an image to a public registry
  • Periodic audit of running containers
  • After a base-image CVE that affects your stack

The UFW bypass — read this first

Docker manipulates iptables directly. By default, ports published with -p are exposed to the world, even if UFW says they should not be. ufw status will mislead you.

Two options:

Option A — use ufw-docker (community-maintained, robust):

# Install ufw-docker
sudo wget -O /usr/local/bin/ufw-docker \
  https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
sudo ufw-docker install
sudo systemctl restart ufw

# Then allow per-container:
sudo ufw-docker allow web 80/tcp

Option B — bind to localhost when you front with a reverse proxy:

# docker-compose.yml — bind to 127.0.0.1, not 0.0.0.0
services:
  app:
    ports:
      - "127.0.0.1:9000:9000"   # host nginx proxies to this

Verify:

sudo ss -tlnp | grep docker     # should not show 0.0.0.0:<port> for internal services

Dockerfile baseline

Every production Dockerfile should:

  1. Run as a non-root user
  2. Pin the base image to a specific digest or version tag (not latest)
  3. Drop build tools from the final image
  4. Not contain secrets baked in
# Pin to specific Node version; consider digest pinning for stricter supply chain
FROM node:20.11.1-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:20.11.1-bookworm-slim
WORKDIR /app

# Create non-root user
RUN groupadd -r app && useradd -r -g app -d /app -s /sbin/nologin app

COPY --from=deps /app/node_modules ./node_modules
COPY --chown=app:app . .

USER app
EXPOSE 3000

# Use exec form so the process becomes PID 1 and receives signals
CMD ["node", "server.js"]

Notes:

  • -slim is much smaller than the default tag. Distroless (gcr.io/distroless/nodejs20-debian12) is smaller and shell-less — harder to live in if an attacker gets RCE
  • Multi-stage to leave compilers / dev deps out of the runtime image
  • --chown on COPY so files are not owned by root inside the image
  • USER last so all later instructions run as that user

Run-time hardening

For each container, prefer the most restrictive run-time options the app actually tolerates.

docker run -d \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \              # only if the app needs to bind <1024
  --security-opt no-new-privileges:true \
  --pids-limit 200 \
  --memory 512m \
  --cpus 1 \
  -p 127.0.0.1:3000:3000 \
  --name myapp myapp:1.2.3

In docker-compose.yml:

services:
  app:
    image: myapp:1.2.3
    read_only: true
    tmpfs:
      - /tmp:size=64m,noexec,nosuid
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]
    security_opt:
      - no-new-privileges:true
    pids_limit: 200
    mem_limit: 512m
    cpus: 1.0
    user: "1000:1000"
    restart: unless-stopped

What each flag does:

  • --read-only — root filesystem cannot be modified. Writable dirs are explicit tmpfs mounts or volumes.
  • --cap-drop=ALL + selective --cap-add — drop all Linux capabilities, add only what the app needs. Most apps need none.
  • --security-opt no-new-privileges — process cannot gain privileges via setuid binaries.
  • --pids-limit — caps fork-bomb DoS.
  • --memory / --cpus — prevents one runaway container from killing the host.

Secrets — never bake them in

Anything in ENV or ARG ends up in image layers. Anyone who can pull the image can extract them.

# Bad
docker build --build-arg STRIPE_KEY=sk_live_... .

# Good — at runtime, from env
docker run -e STRIPE_KEY="$STRIPE_KEY" myapp

# Better — Docker secrets (Swarm) or mounted files
docker run -v /run/secrets/stripe:/run/secrets/stripe:ro myapp
# app reads /run/secrets/stripe

# Best — a real secret manager (Vault, cloud KMS) with short-lived tokens

To inspect a built image for accidental secrets:

docker history --no-trunc myapp:1.2.3
docker save myapp:1.2.3 -o myapp.tar
tar -xf myapp.tar
# search the layer tarballs for .env / known token prefixes

Image scanning

Run a scanner on every image before deploy. Trivy is the most common choice and is fast enough for CI.

trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.2.3

In GitHub Actions:

- name: Build image
  run: docker build -t myapp:${{ github.sha }} .
- name: Scan with Trivy
  uses: aquasecurity/trivy-action@<pinned-sha>
  with:
    image-ref: 'myapp:${{ github.sha }}'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'

Scan on a schedule too — vulnerabilities are discovered after images are built. A weekly re-scan catches newly-known CVEs in already-deployed images.

Network segmentation

Default Docker bridge network puts all containers on one flat L2 segment. They can reach each other freely.

# docker-compose.yml — isolate concerns
networks:
  frontend:
  backend:
    internal: true                # no external connectivity
  database:
    internal: true

services:
  web:
    networks: [frontend, backend]
  api:
    networks: [backend, database]
  db:
    networks: [database]

internal: true networks have no NAT to the outside — only useful for intra-cluster communication. The DB cannot connect out to the internet, which limits exfil if compromised.

Periodic audit

# What is running with --privileged or as root?
docker ps --format 'table {{.Names}}\t{{.Image}}' | tail -n +2 | while read name img; do
  user=$(docker inspect --format '{{.Config.User}}' "$name")
  priv=$(docker inspect --format '{{.HostConfig.Privileged}}' "$name")
  echo "$name  image=$img  user='${user:-<root>}'  privileged=$priv"
done

# Containers with publicly-bound ports
docker ps --format '{{.Names}}\t{{.Ports}}' | grep -E '0\.0\.0\.0|:::' || echo "OK - none"

# Images older than 90 days
docker images --format '{{.Repository}}:{{.Tag}} {{.CreatedSince}}' | grep -E '(months|year)'

# Stopped containers + dangling images — clean up unless you need forensic state
docker container prune -f --filter 'until=720h'   # 30 days
docker image prune -f

Compose-file checklist

Before deploying any docker-compose.yml to production, walk this:

  • No latest tags; versions are pinned (and ideally digest-pinned for critical images)
  • Every service has user: set to a non-root UID:GID
  • read_only: true wherever the app tolerates it (most do)
  • cap_drop: [ALL] + minimal cap_add
  • security_opt: [no-new-privileges:true]
  • mem_limit and pids_limit set
  • Ports bound to 127.0.0.1 unless intentionally public
  • No secrets in environment: — use env_file: (gitignored), Docker secrets, or a manager
  • Internal networks (internal: true) used to keep DB/cache off the public bridge
  • restart: unless-stopped rather than always (so manual stops stick)
  • Volumes are named, not anonymous (named volumes survive docker compose down, anonymous do not)

What this skill will not do

  • Help bypass container isolation on systems you do not own
  • Recommend --privileged, --cap-add=SYS_ADMIN, or running as root unless there is a documented, narrow need
  • Replace a full Kubernetes security review (different stack, different controls)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.