Skill Store 33

Container Security

Container and Kubernetes security: hardening images, runtime configuration and deployment manifests.

by claude-dev-suite · MIT

The skill

Container and Kubernetes security: hardening images, runtime configuration and deployment manifests. USE WHEN: securing Docker containers, Kubernetes deployments, or reviewing container configurations DO NOT USE FOR: general Docker usage (use the `docker` skill), Kubernetes architecture (use the `kubernetes` skill)

Container Security Skill

USE WHEN: Securing Docker containers, Kubernetes deployments, or reviewing container configurations. DO NOT USE FOR: General Docker usage (use docker skill), Kubernetes architecture (use kubernetes skill).

Docker Security Best Practices

Dockerfile Hardening

# Bad: Running as root
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

# Good: Non-root user, minimal image, multi-stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001 -G nodejs

# Copy only necessary files
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

Minimal Base Images

Image Type Security Level Use Case
scratch Highest Go/Rust static binaries
distroless Very High Java, Node.js, Python
alpine High General purpose
slim Medium Debian minimal
full Low Development only
# Distroless for Java
FROM gcr.io/distroless/java21-debian12
COPY --chown=nonroot:nonroot target/app.jar /app.jar
USER nonroot
CMD ["app.jar"]

# Distroless for Node.js
FROM gcr.io/distroless/nodejs20-debian12
COPY --chown=nonroot:nonroot dist/ /app/
WORKDIR /app
USER nonroot
CMD ["server.js"]

Image Scanning

# Trivy (comprehensive)
trivy image myapp:latest
trivy image --severity HIGH,CRITICAL myapp:latest
trivy image --ignore-unfixed myapp:latest

# Docker Scout
docker scout cves myapp:latest
docker scout recommendations myapp:latest

# Grype
grype myapp:latest

# Snyk
snyk container test myapp:latest

Docker Daemon Security

// /etc/docker/daemon.json
{
  "icc": false,
  "userns-remap": "default",
  "no-new-privileges": true,
  "live-restore": true,
  "userland-proxy": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Container Runtime Security

Docker Run Security Options

# Bad: Full privileges
docker run --privileged myapp

# Good: Minimal privileges
docker run \
  --security-opt=no-new-privileges:true \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid \
  --user 1001:1001 \
  --memory=512m \
  --cpu-shares=512 \
  --pids-limit=100 \
  myapp

Capabilities Reference

Capability Purpose Usually Needed
NET_BIND_SERVICE Bind ports < 1024 Sometimes
CHOWN Change file ownership Rarely
SETUID/SETGID Change UID/GID Rarely
SYS_ADMIN Admin operations Never in prod
SYS_PTRACE Debugging Never in prod

Docker Compose Security

version: '3.8'

services:
  app:
    image: myapp:latest
    user: "1001:1001"
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=64m
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
          pids: 100
        reservations:
          memory: 256M
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    networks:
      - internal
    secrets:
      - db_password

  db:
    image: postgres:16-alpine
    user: "70:70"  # postgres user
    volumes:
      - db_data:/var/lib/postgresql/data:rw
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    networks:
      - internal

networks:
  internal:
    driver: bridge
    internal: true

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  db_data:

Kubernetes Security

Pod Security Standards

# Restricted Pod (highest security)
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    runAsGroup: 1001
    fsGroup: 1001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
      resources:
        limits:
          cpu: "500m"
          memory: "512Mi"
          ephemeral-storage: "1Gi"
        requests:
          cpu: "100m"
          memory: "256Mi"
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /app/.cache
  volumes:
    - name: tmp
      emptyDir:
        medium: Memory
        sizeLimit: 64Mi
    - name: cache
      emptyDir:
        sizeLimit: 128Mi
  automountServiceAccountToken: false

Pod Security Admission

# Namespace with enforced security
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Network Policies

# Deny all by default
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

---
# Allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

RBAC Best Practices

# Minimal ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp
  namespace: production
automountServiceAccountToken: false

---
# Minimal Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: myapp-role
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["myapp-config"]
    verbs: ["get"]

---
# RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: myapp-binding
  namespace: production
subjects:
  - kind: ServiceAccount
    name: myapp
    namespace: production
roleRef:
  kind: Role
  name: myapp-role
  apiGroup: rbac.authorization.k8s.io

Secrets Management

# Bad: Plain secret in manifest
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
stringData:
  password: "mysecretpassword"  # Don't commit this!

# Good: External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: db-credentials
  data:
    - secretKey: password
      remoteRef:
        key: secret/data/production/db
        property: password

Container Scanning in CI/CD

# GitHub Actions
name: Container Security
on:
  push:
    branches: [main]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Upload Trivy results
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Dockle lint
        uses: erzz/dockle-action@v1
        with:
          image: myapp:${{ github.sha }}
          failure-threshold: high

Security Scanning Tools Comparison

Tool Focus OSS Enterprise
Trivy Vulnerabilities, misconfig, secrets Yes Aqua
Grype Vulnerabilities Yes Anchore
Snyk Vulnerabilities, license Partial Yes
Docker Scout Vulnerabilities, SBOM Partial Yes
Dockle Best practices Yes -
Hadolint Dockerfile linting Yes -

Quick Reference Commands

# Scan image for vulnerabilities
trivy image --severity HIGH,CRITICAL myapp:latest

# Lint Dockerfile
hadolint Dockerfile

# Check running container security
docker inspect --format='{{.HostConfig.Privileged}}' container_id

# Audit Kubernetes cluster
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}: privileged={.spec.containers[*].securityContext.privileged}{"\n"}{end}'

# Check for root containers
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.name}: runAsNonRoot={.spec.securityContext.runAsNonRoot}{"\n"}{end}'

Use this skill

Connect your agent to the Skill Store MCP server and ask for it by ref: claude-dev-suite/container-security-78757969932.

Download SKILL.md · Markdown version

Details

Publisher
claude-dev-suite
Category
Security › Cloud & infrastructure security
License
MIT
Updated
2026-10-19

References

More from claude-dev-suite