container-security

SKILL.md

Container Security

Overview

This skill covers security best practices for containerized applications, including Docker image hardening, Kubernetes security configurations, image vulnerability scanning, and runtime protection.

Keywords: container security, Docker, Kubernetes, image scanning, Dockerfile, pod security, network policies, RBAC, container runtime, Trivy, Falco, gVisor, seccomp, AppArmor, distroless, rootless containers

When to Use This Skill

  • Building secure Docker images
  • Configuring Kubernetes pod security
  • Setting up container vulnerability scanning
  • Implementing Kubernetes RBAC
  • Configuring network policies
  • Managing secrets in Kubernetes
  • Setting up runtime security monitoring

Container Security Layers

Layer Controls Tools
Image Minimal base, vulnerability scanning, signing Trivy, Cosign, Grype
Build Multi-stage builds, non-root, no secrets Docker, Buildah, Kaniko
Registry Scanning, signing verification, access control Harbor, ECR, ACR
Runtime Seccomp, AppArmor, read-only root gVisor, Kata, Falco
Orchestration Pod security, RBAC, network policies Kubernetes, OPA/Gatekeeper
Secrets Encrypted at rest, external providers Vault, Sealed Secrets, ESO

Secure Dockerfile Patterns

Minimal Secure Dockerfile

# Use specific version, not :latest
FROM node:20.10-alpine3.19 AS builder

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

WORKDIR /app

# Copy dependency files first (layer caching)
COPY package*.json ./

# Install dependencies with security flags
RUN npm ci --only=production --ignore-scripts && \
    npm cache clean --force

# Copy application code
COPY --chown=appuser:appgroup . .

# Build if needed
RUN npm run build

# --- Production Stage ---
FROM node:20.10-alpine3.19 AS production

# Security: Don't run as root
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

# Security: Remove unnecessary packages
RUN apk --no-cache add dumb-init && \
    rm -rf /var/cache/apk/*

WORKDIR /app

# Copy only production artifacts
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./

# Security: Read-only filesystem support
RUN mkdir -p /app/tmp && chown appuser:appgroup /app/tmp

# Switch to non-root user
USER appuser

# Security: Use dumb-init to handle signals
ENTRYPOINT ["dumb-init", "--"]

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD node healthcheck.js || exit 1

# Expose port (non-privileged)
EXPOSE 3000

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

Distroless Production Image

# Build stage with full toolchain
FROM golang:1.22-alpine AS builder

RUN apk add --no-cache git ca-certificates

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .

# Build static binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags='-w -s -extldflags "-static"' \
    -o /app/server ./cmd/server

# --- Distroless production image ---
FROM gcr.io/distroless/static-debian12:nonroot

# Copy binary and CA certs
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server

# Run as non-root (65532 is the nonroot user in distroless)
USER 65532:65532

EXPOSE 8080

ENTRYPOINT ["/server"]

Image Scanning

Trivy Scanning

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

# Scan with SBOM generation
trivy image --format cyclonedx --output sbom.json myapp:latest

# Scan filesystem (for CI before building)
trivy fs --security-checks vuln,secret,config .

# Scan with exit code for CI
trivy image --exit-code 1 --severity CRITICAL myapp:latest

# Ignore unfixed vulnerabilities
trivy image --ignore-unfixed myapp:latest

CI Pipeline Image Scanning

# .github/workflows/container-security.yml
name: Container Security

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

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

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

      - name: Run Trivy vulnerability scanner
        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 scan results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

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

      - name: Sign image with Cosign
        if: github.ref == 'refs/heads/main'
        env:
          COSIGN_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
        run: |
          cosign sign --key env://COSIGN_KEY myapp:${{ github.sha }}

Kubernetes Pod Security

Pod Security Standards (PSS)

# Enforce Pod Security Standards at namespace level
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    # Enforce restricted policy
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    # Warn on baseline violations
    pod-security.kubernetes.io/warn: baseline
    pod-security.kubernetes.io/warn-version: latest
    # Audit all violations
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest

Secure Pod Specification

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  labels:
    app: secure-app
spec:
  # Prevent privilege escalation across containers
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault

  # Service account with minimal permissions
  serviceAccountName: app-minimal-sa
  automountServiceAccountToken: false

  containers:
    - name: app
      image: myapp:v1.0.0@sha256:abc123...
      imagePullPolicy: Always

      # Container-level security context
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        runAsNonRoot: true
        runAsUser: 1000
        capabilities:
          drop:
            - ALL
        seccompProfile:
          type: RuntimeDefault

      # Resource limits (prevent DoS)
      resources:
        limits:
          cpu: "500m"
          memory: "256Mi"
          ephemeral-storage: "100Mi"
        requests:
          cpu: "100m"
          memory: "128Mi"

      # Writable directories via emptyDir
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /app/cache

      # Health probes
      livenessProbe:
        httpGet:
          path: /health
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 10
      readinessProbe:
        httpGet:
          path: /ready
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 5

  volumes:
    - name: tmp
      emptyDir:
        sizeLimit: 10Mi
    - name: cache
      emptyDir:
        sizeLimit: 50Mi

  # DNS policy for security
  dnsPolicy: ClusterFirst

  # Host settings (all disabled for security)
  hostNetwork: false
  hostPID: false
  hostIPC: false

Network Policies

Default Deny All

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

Application-Specific Policy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-network-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
    - Egress

  ingress:
    # Allow from ingress controller only
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
          podSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
      ports:
        - protocol: TCP
          port: 8080

    # Allow from specific services
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

  egress:
    # Allow DNS
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53

    # Allow database access
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

    # Allow external HTTPS
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8
              - 172.16.0.0/12
              - 192.168.0.0/16
      ports:
        - protocol: TCP
          port: 443

Kubernetes RBAC

Minimal Service Account

# Service account with no auto-mounted token
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-minimal-sa
  namespace: production
automountServiceAccountToken: false
---
# Role with minimal permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-role
  namespace: production
rules:
  # Only allow reading configmaps
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["app-config"]
    verbs: ["get"]
  # Only allow reading specific secrets
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["app-credentials"]
    verbs: ["get"]
---
# Bind role to service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-role-binding
  namespace: production
subjects:
  - kind: ServiceAccount
    name: app-minimal-sa
    namespace: production
roleRef:
  kind: Role
  name: app-role
  apiGroup: rbac.authorization.k8s.io

Audit RBAC Permissions

# List all cluster-admin bindings (high risk)
kubectl get clusterrolebindings -o json | jq '.items[] |
  select(.roleRef.name == "cluster-admin") |
  {name: .metadata.name, subjects: .subjects}'

# Check service account permissions
kubectl auth can-i --list --as=system:serviceaccount:production:app-minimal-sa

# Find overly permissive roles (using wildcards)
kubectl get roles,clusterroles -A -o json | jq '.items[] |
  select(.rules[]?.resources[]? == "*" or .rules[]?.verbs[]? == "*") |
  {name: .metadata.name, namespace: .metadata.namespace}'

Secrets Management

External Secrets Operator

# SecretStore pointing to HashiCorp Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "production-role"
          serviceAccountRef:
            name: "vault-auth-sa"
---
# External Secret syncing from Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
    template:
      type: Opaque
      data:
        DATABASE_URL: "{{ .database_url }}"
        API_KEY: "{{ .api_key }}"
  data:
    - secretKey: database_url
      remoteRef:
        key: production/app
        property: database_url
    - secretKey: api_key
      remoteRef:
        key: production/app
        property: api_key

Sealed Secrets

# Install sealed-secrets controller
helm install sealed-secrets sealed-secrets/sealed-secrets \
  --namespace kube-system

# Seal a secret
kubectl create secret generic my-secret \
  --from-literal=password=supersecret \
  --dry-run=client -o yaml | \
  kubeseal --format yaml > sealed-secret.yaml

# The sealed secret can be safely committed to git
cat sealed-secret.yaml

Runtime Security

Falco Rules

# Custom Falco rules
- rule: Unauthorized Process in Container
  desc: Detect unauthorized processes running in containers
  condition: >
    spawned_process and
    container and
    not proc.name in (allowed_processes) and
    not container.image.repository in (trusted_images)
  output: >
    Unauthorized process started (user=%user.name command=%proc.cmdline
    container=%container.name image=%container.image.repository)
  priority: WARNING
  tags: [container, process]

- rule: Write to Sensitive Directories
  desc: Detect writes to sensitive directories in containers
  condition: >
    open_write and
    container and
    (fd.name startswith /etc/ or
     fd.name startswith /bin/ or
     fd.name startswith /sbin/ or
     fd.name startswith /usr/bin/)
  output: >
    Write to sensitive directory (file=%fd.name user=%user.name
    container=%container.name image=%container.image.repository)
  priority: ERROR
  tags: [container, filesystem]

- rule: Container Shell Spawned
  desc: Detect shell spawned in container
  condition: >
    spawned_process and
    container and
    proc.name in (shell_binaries) and
    not proc.pname in (allowed_shell_parents)
  output: >
    Shell spawned in container (user=%user.name shell=%proc.name
    parent=%proc.pname container=%container.name)
  priority: WARNING
  tags: [container, shell]

- list: shell_binaries
  items: [bash, sh, zsh, ash, dash, ksh, tcsh, csh]

- list: allowed_shell_parents
  items: [crond, sshd, sudo]

Quick Reference

Dockerfile Security Checklist

Check Command/Pattern
No latest tag FROM image:specific-version
Non-root user USER 1000
No secrets in image trivy fs --security-checks secret .
Multi-stage build Separate builder and production stages
Read-only filesystem --read-only or readOnlyRootFilesystem: true
Minimal base image Alpine, distroless, or scratch
Signed image cosign sign / cosign verify

Kubernetes Security Checklist

Check Setting
Non-root runAsNonRoot: true
No privilege escalation allowPrivilegeEscalation: false
Drop capabilities capabilities: {drop: [ALL]}
Read-only root readOnlyRootFilesystem: true
Resource limits resources.limits defined
Network policies Default deny + explicit allow
Seccomp profile seccompProfile: {type: RuntimeDefault}
No host namespaces hostNetwork/PID/IPC: false

References

  • Dockerfile Hardening: See references/dockerfile-security.md for detailed patterns
  • Kubernetes Security: See references/kubernetes-security.md for comprehensive K8s guidance
  • Container Scanning: See references/container-scanning.md for scanner configurations

Last Updated: 2025-12-26

Weekly Installs
4
Installed on
antigravity3
windsurf2
trae2
opencode2
codex2
claude-code2