docker-container-security — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited docker-container-security (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
docker-compose.yml for productionDocker 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/tcpOption 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 thisVerify:
sudo ss -tlnp | grep docker # should not show 0.0.0.0:<port> for internal servicesEvery production Dockerfile should:
latest)# 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:
gcr.io/distroless/nodejs20-debian12) is smaller and shell-less — harder to live in if an attacker gets RCEFor 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.3In 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-stoppedWhat each flag does:
tmpfs mounts or volumes.setuid binaries.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 tokensTo 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 prefixesRun 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.3In 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.
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.
# 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 -fBefore deploying any docker-compose.yml to production, walk this:
latest tags; versions are pinned (and ideally digest-pinned for critical images)user: set to a non-root UID:GIDread_only: true wherever the app tolerates it (most do)cap_drop: [ALL] + minimal cap_addsecurity_opt: [no-new-privileges:true]mem_limit and pids_limit set127.0.0.1 unless intentionally publicenvironment: — use env_file: (gitignored), Docker secrets, or a managerinternal: true) used to keep DB/cache off the public bridgerestart: unless-stopped rather than always (so manual stops stick)docker compose down, anonymous do not)--privileged, --cap-add=SYS_ADMIN, or running as root unless there is a documented, narrow need~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.