
Security Docker
- 99 installs
- 125 repo stars
- Updated February 4, 2026
- igorwarzocha/opencode-workflows
Audit Dockerfiles and compose files for secrets in layers, exposed ports, privileged containers, and non-root users.
About
A security-audit skill for Docker images and container deployments. A developer uses it to check for secrets in ENV/ARG, accidentally exposed databases, and privileged/docker.sock mounts.
- Flags secrets in build args/ENV visible in image history
- Recommends multi-stage builds, minimal base images, and non-root USER
Security Docker by the numbers
- 99 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,009 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/igorwarzocha/opencode-workflows --skill security-dockerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 99 |
|---|---|
| repo stars | ★ 125 |
| Last updated | February 4, 2026 |
| Repository | igorwarzocha/opencode-workflows ↗ |
What it does
Audit Dockerfiles and compose files for secrets in layers, exposed ports, privileged containers, and non-root users.
Files
<overview>
Security audit patterns for Docker and container deployments covering secrets in images, port exposure, user privileges, and compose security.
</overview>
<vulnerabilities>
Secrets in Images (Critical)
Secrets in Build Args/ENV
# ❌ CRITICAL: Secret in ENV (visible in image history)
ENV API_KEY=sk_live_abc123
ENV DATABASE_URL=postgres://user:password@host/db
# ❌ CRITICAL: Secret in ARG (visible in image history)
ARG AWS_SECRET_ACCESS_KEY
RUN aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
# ✓ Use runtime secrets
# Pass via docker run -e or docker-compose environment/env_file
# ✓ Docker secrets (Swarm) or orchestrator-specific secrets
# Use /run/secrets/* instead of ENV/ARG when availableSecrets Baked into Layers
# ❌ CRITICAL: Even if deleted, secret is in layer history
COPY .env /app/.env
RUN source /app/.env && do_something
RUN rm /app/.env # Still in previous layer!
# ❌ CRITICAL: Copying all files includes secrets
COPY . /app/ # Copies .env, .git, etc.
# ✓ Use .dockerignore
# In .dockerignore:
# .env*
# .git
# *.pem
# *.key
# ✓ Or explicit COPY
COPY package*.json /app/
COPY src/ /app/src/Checking Image History
# Audit existing images for secrets
docker history --no-trunc <image>
docker inspect <image> | jq '.[0].Config.Env'Port Exposure
docker-compose.yml
# ❌ CRITICAL: Database exposed to host network
services:
db:
image: postgres
ports:
- "5432:5432" # Accessible from outside!
# ❌ CRITICAL: Redis without password
redis:
image: redis
ports:
- "6379:6379" # And no AUTH!
# ✓ Internal only (accessible to other containers)
services:
db:
image: postgres
expose:
- "5432" # Only internal
# No 'ports' = not exposed to host
# ✓ If must expose, bind to localhost
db:
ports:
- "127.0.0.1:5432:5432" # Only localhostDefault Credentials
# ❌ No password or default password
services:
db:
image: postgres
environment:
POSTGRES_PASSWORD: postgres # Default!
redis:
image: redis
# No password at all
# ✓ Strong passwords from secrets
services:
db:
image: postgres
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt # MUST NOT be in git!Non-Root User
# ❌ Running as root (default)
FROM node:18
COPY . /app
CMD ["node", "server.js"] # Runs as root
# ✓ Create and use non-root user
FROM node:18
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
# ✓ Using numeric UID (more portable)
FROM node:18
RUN useradd -r -u 1001 appuser
WORKDIR /app
COPY --chown=1001:1001 . .
USER 1001
CMD ["node", "server.js"]Multi-Stage Builds
# ❌ Build tools and secrets in final image
FROM node:18
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
# Final image has: source, node_modules (dev deps), build tools
# ✓ Multi-stage: only production artifacts
FROM node:18 AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-slim AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]
# Final image: minimal, no source, no build toolsDocker Compose Security
Privileged Mode
# ❌ CRITICAL: Full host access
services:
app:
privileged: true # Container can do anything on host!
# ❌ HIGH: Dangerous capabilities
services:
app:
cap_add:
- SYS_ADMIN
- NET_ADMINVolume Mounts
# ❌ CRITICAL: Docker socket access = root on host
services:
app:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# ❌ HIGH: Sensitive host paths
services:
app:
volumes:
- /etc:/etc
- /root:/rootNetwork Mode
# ❌ HIGH: Host network mode
services:
app:
network_mode: host # Bypasses Docker network isolationImage Security
Base Image
# ❌ Outdated or unverified
FROM node:14 # EOL version
FROM random-user/node-app # Unverified
# ✓ Official, recent, minimal
FROM node:20-slim
FROM node:20-alpineImage Scanning
# Scan for vulnerabilities
docker scout cves <image>
trivy image <image>
grype <image></vulnerabilities>
<commands>
Quick Audit Commands
# Find secrets in Dockerfile
rg "(ENV|ARG).*(KEY|SECRET|PASSWORD|TOKEN)" Dockerfile*
# Find exposed ports in compose
rg "ports:" docker-compose*.yml -A 3
# Check for privileged/capabilities
rg "(privileged|cap_add|network_mode)" docker-compose*.yml
# Check for docker.sock mount
rg "docker.sock" docker-compose*.yml
# Check for USER instruction
grep "^USER" Dockerfile
# Check .dockerignore exists and has secrets
cat .dockerignore | grep -E "(env|key|secret|pem)"</commands>
<checklist>
Hardening Checklist
- [ ] No secrets in ENV/ARG instructions
- [ ] No secrets COPY'd into image
- [ ] .dockerignore excludes .env, .git, .pem, .key
- [ ] Database/Redis ports not exposed to host (or only 127.0.0.1)
- [ ] Strong passwords for all services (not defaults)
- [ ] USER instruction sets non-root user
- [ ] Multi-stage build for production images
- [ ] No privileged: true
- [ ] No docker.sock mount (unless required)
- [ ] Base images are official and recent
- [ ] Images scanned for vulnerabilities
</checklist>
#!/usr/bin/env bash
# Docker Security Scanner - First-pass automated detection
# Usage: ./scan.sh [directory]
set -euo pipefail
DIR="${1:-.}"
FOUND=0
echo "=== DOCKER SECURITY SCAN ==="
echo "Directory: $DIR"
echo "Timestamp: $(date -Iseconds)"
echo ""
if ! command -v rg &> /dev/null; then
echo "[ERROR] ripgrep (rg) required"
exit 1
fi
report() {
local severity="$1"
local title="$2"
local file="${3:-}"
local line="${4:-}"
echo "[$severity] $title"
if [[ -n "$file" ]]; then
if [[ -n "$line" ]]; then
echo " File: $file:$line"
else
echo " File: $file"
fi
fi
echo ""
FOUND=$((FOUND + 1))
}
# Find Dockerfiles
DOCKERFILES=$(find "$DIR" -name "Dockerfile*" -type f 2>/dev/null || true)
COMPOSE_FILES=$(find "$DIR" -name "docker-compose*.yml" -o -name "docker-compose*.yaml" -o -name "compose.yml" -o -name "compose.yaml" 2>/dev/null | head -10 || true)
echo "=== DOCKERFILE CHECKS ==="
echo ""
for dockerfile in $DOCKERFILES; do
[[ -z "$dockerfile" ]] && continue
echo "## Checking: $dockerfile"
# Secrets in ENV
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "CRITICAL" "Secret in ENV instruction" "$dockerfile" "$line"
done < <(rg -n 'ENV.*(KEY|SECRET|PASSWORD|TOKEN)' "$dockerfile" 2>/dev/null || true)
# Secrets in ARG
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "HIGH" "Sensitive ARG instruction (visible in image history)" "$dockerfile" "$line"
done < <(rg -n 'ARG.*(KEY|SECRET|PASSWORD|TOKEN)' "$dockerfile" 2>/dev/null || true)
# COPY . (copies everything)
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "MEDIUM" "COPY . copies all files (may include secrets)" "$dockerfile" "$line"
done < <(rg -n '^COPY \. ' "$dockerfile" 2>/dev/null || true)
# No USER instruction
if ! rg -q '^USER ' "$dockerfile" 2>/dev/null; then
report "MEDIUM" "No USER instruction (runs as root)" "$dockerfile"
fi
# Outdated base images
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
if echo "$match" | grep -qE 'node:(14|16)[^0-9]|python:(3\.7|3\.8)[^0-9]'; then
report "MEDIUM" "Potentially outdated base image" "$dockerfile" "$line"
fi
done < <(rg -n '^FROM ' "$dockerfile" 2>/dev/null || true)
echo ""
done
echo "=== DOCKER-COMPOSE CHECKS ==="
echo ""
for compose in $COMPOSE_FILES; do
[[ -z "$compose" ]] && continue
echo "## Checking: $compose"
# Exposed database ports
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
if echo "$match" | grep -qE '"?(5432|3306|27017|6379):'; then
report "CRITICAL" "Database/Redis port exposed to host" "$compose" "$line"
fi
done < <(rg -n 'ports:' "$compose" -A 5 2>/dev/null || true)
# Docker socket mount
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "CRITICAL" "Docker socket mounted (root access to host)" "$compose" "$line"
done < <(rg -n 'docker\.sock' "$compose" 2>/dev/null || true)
# Privileged mode
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "CRITICAL" "Privileged container (full host access)" "$compose" "$line"
done < <(rg -n 'privileged.*true' "$compose" 2>/dev/null || true)
# Dangerous capabilities
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "HIGH" "cap_add includes privileged capability (review necessity)" "$compose" "$line"
done < <(rg -n '(SYS_ADMIN|NET_ADMIN|SYS_PTRACE|ALL)' "$compose" 2>/dev/null || true)
# Default passwords
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
if echo "$match" | grep -qiE 'PASSWORD.*(postgres|mysql|admin|root|password|123)'; then
report "HIGH" "Potential default/weak password" "$compose" "$line"
fi
done < <(rg -n 'PASSWORD' "$compose" 2>/dev/null || true)
# Network mode host
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "HIGH" "Host network mode (bypasses Docker networking)" "$compose" "$line"
done < <(rg -n 'network_mode.*host' "$compose" 2>/dev/null || true)
# Host PID namespace
while IFS=: read -r line match; do
[[ -z "$line" ]] && continue
report "HIGH" "PID namespace set to host (process isolation bypass)" "$compose" "$line"
done < <(rg -n 'pid:\s*host' "$compose" 2>/dev/null || true)
echo ""
done
echo "=== .dockerignore CHECK ==="
echo ""
if [[ -f "$DIR/.dockerignore" ]]; then
echo "[INFO] .dockerignore found"
# Check for important exclusions
for pattern in ".env" ".git" "*.pem" "*.key"; do
if ! grep -q "$pattern" "$DIR/.dockerignore" 2>/dev/null; then
report "MEDIUM" ".dockerignore missing: $pattern" "$DIR/.dockerignore"
fi
done
else
if [[ -n "$DOCKERFILES" ]]; then
report "MEDIUM" "No .dockerignore file (secrets may be copied into image)"
fi
fi
echo "=== SUMMARY ==="
if [[ $FOUND -gt 0 ]]; then
echo "[!] Found $FOUND potential issues. Review above."
exit 1
else
echo "[✓] No obvious Docker security issues detected"
exit 0
fi