Introduction
Containers have revolutionized application deployment, but they've also introduced new security challenges. While containers provide isolation, they're not a security silver bullet. A misconfigured container can expose your entire infrastructure to attack, leak sensitive data, or become a foothold for lateral movement within your network.
In this comprehensive guide, we'll explore container security best practices across the entire software development lifecycle—from writing Dockerfiles to running containers in production. Whether you're running Docker locally or managing thousands of containers in Kubernetes, these practices will help you build a robust security posture.
Understanding Container Security Risks
Before diving into solutions, let's understand the unique security challenges containers present:
Image Vulnerabilities
Container images often contain outdated packages with known vulnerabilities. A study by Snyk found that 75% of Docker Hub images contain at least one critical vulnerability.
Overprivileged Containers
Many containers run as root by default, violating the principle of least privilege. If compromised, attackers gain root access within the container.
Insecure Image Registries
Public registries contain malicious images disguised as legitimate software. Even private registries can be misconfigured to allow unauthorized access.
Secrets in Images
Developers accidentally commit credentials, API keys, and certificates into container images, exposing them to anyone with image access.
Container Escape
While rare, vulnerabilities in container runtimes can allow attackers to escape containers and access the host system.
Supply Chain Attacks
Base images from untrusted sources might contain backdoors or malware, compromising your entire application stack.
Secure Docker Image Development
Start with Minimal Base Images
Use minimal base images to reduce attack surface. Distroless or Alpine images contain far fewer packages than full OS images.
# Bad: Full Ubuntu image (78MB, hundreds of packages) FROM ubuntu:22.04 # Better: Alpine image (5MB, minimal packages) FROM alpine:3.18 # Best: Distroless image (smallest, no shell, no package manager) FROM gcr.io/distroless/nodejs18-debian11 Multi-Stage Builds for Smaller Images
Use multi-stage builds to keep build tools out of production images:
# Build stage FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build # Production stage FROM gcr.io/distroless/nodejs18-debian11 WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY package*.json ./ USER nonroot CMD ["node", "dist/index.js"] Never Run as Root
Create and use a non-root user in your Dockerfile:
FROM node:18-alpine # Create app user RUN addgroup -g 1001 -S appuser && \ adduser -u 1001 -S appuser -G appuser # Set ownership WORKDIR /app COPY --chown=appuser:appuser . . # Install dependencies as root (needed for npm install) RUN npm ci --only=production # Switch to non-root user USER appuser # Run as non-root CMD ["node", "index.js"] Scan Images for Vulnerabilities
Integrate vulnerability scanning into your CI/CD pipeline:
# Using Trivy docker run aquasec/trivy image myapp:latest # Using Snyk snyk container test myapp:latest # Using Grype grype myapp:latest # GitHub Actions example name: Container Security Scan on: push: branches: [main] pull_request: branches: [main] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - 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' - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - name: Fail on high severity vulnerabilities uses: aquasecurity/trivy-action@master with: image-ref: 'myapp:${{ github.sha }}' exit-code: '1' severity: 'CRITICAL,HIGH' Don't Store Secrets in Images
Never bake secrets into images. Use environment variables, secret management tools, or mount secrets at runtime:
# Bad: Secret hardcoded in image ENV API_KEY="sk-1234567890abcdef" # Bad: Secret copied into image COPY secrets.env /app/ # Good: Secret passed at runtime # docker run -e API_KEY="$API_KEY" myapp # Better: Secret from secrets management # Kubernetes Secret, AWS Secrets Manager, HashiCorp Vault # Kubernetes: Mount secrets as environment variables apiVersion: v1 kind: Pod metadata: name: myapp spec: containers: - name: app image: myapp:latest env: - name: API_KEY valueFrom: secretKeyRef: name: myapp-secrets key: api-key Use .dockerignore
Prevent sensitive files from being copied into images:
# .dockerignore .git .env .env.local *.log node_modules npm-debug.log secrets/ *.pem *.key .aws .gcp Pin Image Versions
Always use specific image versions, never latest:
# Bad: Unpredictable, can break without warning FROM node:latest # Bad: Still somewhat unpredictable FROM node:18 # Good: Specific version FROM node:18.17.1 # Best: Specific version + digest (immutable) FROM node:18.17.1-alpine@sha256:f1657204d3463bce763cefa5b25e48c28af6eb183b4b6b06f7d64a8e2f18314 Limit Image Layer Count
Combine RUN commands to reduce layers and image size:
# Bad: Many layers RUN apt-get update RUN apt-get install -y curl RUN apt-get install -y git RUN apt-get clean RUN rm -rf /var/lib/apt/lists/* # Good: Single layer RUN apt-get update && \ apt-get install -y --no-install-recommends curl git && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* Container Runtime Security
Use Read-Only Filesystems
Make container filesystems read-only to prevent tampering:
# Docker Compose services: app: image: myapp:latest read_only: true tmpfs: - /tmp - /var/run # Kubernetes apiVersion: v1 kind: Pod metadata: name: myapp spec: containers: - name: app image: myapp:latest securityContext: readOnlyRootFilesystem: true volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: {} Drop Unnecessary Capabilities
Linux capabilities allow fine-grained privilege control. Drop all capabilities, then add only what's needed:
# Kubernetes Pod with minimal capabilities apiVersion: v1 kind: Pod metadata: name: myapp spec: containers: - name: app image: myapp:latest securityContext: capabilities: drop: - ALL add: - NET_BIND_SERVICE # Only if binding to ports <1024 allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1001 seccompProfile: type: RuntimeDefault Resource Limits
Prevent container resource exhaustion attacks:
# Docker Compose services: app: image: myapp:latest deploy: resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.5' memory: 256M # Kubernetes resources: requests: memory: "256Mi" cpu: "500m" limits: memory: "512Mi" cpu: "1000m" Use Security Profiles
AppArmor and Seccomp restrict system calls containers can make:
# Kubernetes with AppArmor and Seccomp apiVersion: v1 kind: Pod metadata: name: myapp annotations: container.apparmor.security.beta.kubernetes.io/app: runtime/default spec: containers: - name: app image: myapp:latest securityContext: seccompProfile: type: RuntimeDefault Network Segmentation
Isolate containers with network policies:
# Kubernetes NetworkPolicy apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: myapp-netpol spec: podSelector: matchLabels: app: myapp policyTypes: - Ingress - Egress ingress: # Only allow traffic from specific pods - from: - podSelector: matchLabels: app: frontend ports: - protocol: TCP port: 8080 egress: # Only allow traffic to database - to: - podSelector: matchLabels: app: postgres ports: - protocol: TCP port: 5432 # Allow DNS - to: - namespaceSelector: matchLabels: name: kube-system ports: - protocol: UDP port: 53 Registry Security
Use Private Registries
Host images in private registries with access control:
# AWS ECR aws ecr get-login-password --region us-east-1 | \ docker login --username AWS --password-stdin \ 123456789.dkr.ecr.us-east-1.amazonaws.com # Google Container Registry gcloud auth configure-docker # Azure Container Registry az acr login --name myregistry Sign Images
Use Docker Content Trust or Notary to sign images:
# Enable Docker Content Trust export DOCKER_CONTENT_TRUST=1 # Push signed image docker push myregistry.com/myapp:v1.0 # Verify signature on pull docker pull myregistry.com/myapp:v1.0 Implement Image Scanning in Registry
Many registries provide built-in vulnerability scanning:
# AWS ECR: Enable scanning on push aws ecr put-image-scanning-configuration \ --repository-name myapp \ --image-scanning-configuration scanOnPush=true # Google Artifact Registry: Enable vulnerability scanning gcloud artifacts repositories update myrepo \ --location=us-central1 \ --enable-scanning Use Admission Controllers
Prevent unsigned or vulnerable images from running:
# OPA Gatekeeper policy: Only allow images from approved registries apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sAllowedRepos metadata: name: allowed-repos spec: match: kinds: - apiGroups: [""] kinds: ["Pod"] - apiGroups: ["apps"] kinds: ["Deployment", "StatefulSet"] parameters: repos: - "myregistry.com/" - "gcr.io/myproject/" Kubernetes-Specific Security
Pod Security Standards
Enforce pod security policies cluster-wide:
# Restricted Pod Security Standard (most secure) apiVersion: v1 kind: Namespace metadata: name: production labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted RBAC for Least Privilege
Grant minimal permissions to service accounts:
apiVersion: v1 kind: ServiceAccount metadata: name: myapp-sa namespace: production --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: myapp-role namespace: production rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list"] - apiGroups: [""] resources: ["secrets"] resourceNames: ["myapp-secrets"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: myapp-rolebinding namespace: production subjects: - kind: ServiceAccount name: myapp-sa namespace: production roleRef: kind: Role name: myapp-role apiGroup: rbac.authorization.k8s.io Secrets Encryption at Rest
Encrypt Kubernetes secrets in etcd:
# /etc/kubernetes/encryption-config.yaml apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key1 secret: <BASE64_ENCODED_SECRET> - identity: {} Audit Logging
Enable comprehensive audit logging:
apiVersion: audit.k8s.io/v1 kind: Policy rules: # Log admin actions - level: RequestResponse users: ["admin"] # Log secret access - level: Metadata resources: - group: "" resources: ["secrets"] # Log pod exec/attach - level: Request verbs: ["exec", "attach"] resources: - group: "" resources: ["pods/exec", "pods/attach"] Runtime Security Monitoring
Use Falco for Threat Detection
Falco monitors kernel events to detect anomalous behavior:
# Falco rule: Detect shell in container - rule: Terminal Shell in Container desc: A shell was spawned in a container condition: > spawned_process and container and proc.name in (shell_binaries) output: > Shell spawned in container (user=%user.name container=%container.name shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline) priority: WARNING # Detect outbound connections to suspicious IPs - rule: Outbound Connection to Suspicious IP desc: Container connected to known malicious IP condition: > outbound and container and fd.sip in (suspicious_ips) output: > Suspicious outbound connection (container=%container.name destination=%fd.sip:%fd.sport command=%proc.cmdline) priority: CRITICAL Container Runtime Security Tools
# Deploy Falco as DaemonSet apiVersion: apps/v1 kind: DaemonSet metadata: name: falco namespace: falco spec: selector: matchLabels: app: falco template: metadata: labels: app: falco spec: serviceAccount: falco hostNetwork: true hostPID: true containers: - name: falco image: falcosecurity/falco:latest securityContext: privileged: true volumeMounts: - mountPath: /host/var/run/docker.sock name: docker-socket - mountPath: /host/dev name: dev-fs - mountPath: /host/proc name: proc-fs readOnly: true volumes: - name: docker-socket hostPath: path: /var/run/docker.sock - name: dev-fs hostPath: path: /dev - name: proc-fs hostPath: path: /proc Secrets Management
External Secrets Operator
Sync secrets from external vaults into Kubernetes:
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: myapp-secrets spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: SecretStore target: name: myapp-secrets creationPolicy: Owner data: - secretKey: database-password remoteRef: key: prod/myapp/db-password - secretKey: api-key remoteRef: key: prod/myapp/api-key Sealed Secrets
Encrypt secrets in Git for GitOps workflows:
# Encrypt secret echo -n 'supersecret' | kubectl create secret generic mysecret \ --dry-run=client --from-file=password=/dev/stdin -o yaml | \ kubeseal -o yaml > mysealedsecret.yaml # Commit encrypted secret to Git git add mysealedsecret.yaml git commit -m "Add sealed secret" Security Scanning and Compliance
Comprehensive Security Scanning Pipeline
# Complete security scanning pipeline name: Security Pipeline on: push: branches: [main] pull_request: branches: [main] jobs: security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 # 1. Scan source code for secrets - name: Secret scanning uses: trufflesecurity/trufflehog@main with: path: ./ # 2. SAST - Static Application Security Testing - name: Run Semgrep uses: returntocorp/semgrep-action@v1 # 3. Dependency scanning - name: Run Snyk uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} # 4. Build image - name: Build Docker image run: docker build -t myapp:${{ github.sha }} . # 5. Container scanning - name: Run Trivy uses: aquasecurity/trivy-action@master with: image-ref: 'myapp:${{ github.sha }}' severity: 'CRITICAL,HIGH' exit-code: '1' # 6. IaC scanning - name: Run Checkov uses: bridgecrewio/checkov-action@master with: directory: k8s/ framework: kubernetes # 7. License compliance - name: License scanning run: | npm install -g license-checker license-checker --production --onlyAllow 'MIT;Apache-2.0;BSD-3-Clause' Compliance and Hardening
CIS Benchmark Compliance
Follow CIS Docker and Kubernetes benchmarks:
# Run Docker Bench Security docker run --rm --net host --pid host --userns host --cap-add audit_control \ -v /var/lib:/var/lib \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /etc:/etc \ docker/docker-bench-security # Run kube-bench for Kubernetes kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml kubectl logs job/kube-bench Incident Response
Container Forensics
When a container is compromised:
# 1. Don't delete the container immediately # Pause it to preserve state docker pause <container_id> # 2. Export filesystem for analysis docker export <container_id> > compromised-container.tar # 3. Inspect container configuration docker inspect <container_id> > container-config.json # 4. Check logs docker logs <container_id> > container-logs.txt # 5. Review network connections docker exec <container_id> netstat -antp # 6. Check running processes docker top <container_id> # 7. After analysis, kill and remove docker kill <container_id> docker rm <container_id> Best Practices Checklist
Development Phase
- [ ] Use minimal base images (Alpine, Distroless)
- [ ] Implement multi-stage builds
- [ ] Run containers as non-root user
- [ ] Pin image versions with digests
- [ ] Use .dockerignore to exclude sensitive files
- [ ] Never commit secrets to images
- [ ] Scan images for vulnerabilities
- [ ] Keep images under 500MB
- [ ] Document security considerations
Build Phase
- [ ] Automated vulnerability scanning in CI/CD
- [ ] SAST scanning for code vulnerabilities
- [ ] Dependency scanning for vulnerable packages
- [ ] Secret scanning to prevent leaks
- [ ] License compliance checking
- [ ] Sign images before pushing to registry
Registry Phase
- [ ] Use private registries with authentication
- [ ] Enable vulnerability scanning on push
- [ ] Implement image retention policies
- [ ] Regular security audits of registry access
- [ ] Enable image signing and verification
Runtime Phase
- [ ] Use read-only filesystems
- [ ] Drop unnecessary Linux capabilities
- [ ] Enable security profiles (AppArmor/Seccomp)
- [ ] Implement resource limits
- [ ] Use network policies for isolation
- [ ] Enable audit logging
- [ ] Deploy runtime security monitoring (Falco)
- [ ] Regular security updates and patching
Kubernetes Phase
- [ ] Enforce Pod Security Standards
- [ ] Implement RBAC with least privilege
- [ ] Encrypt secrets at rest
- [ ] Use admission controllers
- [ ] Enable audit logging
- [ ] Regular security updates
- [ ] Network policies for pod isolation
- [ ] Use external secrets management
Conclusion
Container security is not a one-time task but an ongoing process that spans the entire software development lifecycle. By implementing security best practices from development through production, you can significantly reduce your attack surface and protect your infrastructure.
Key takeaways:
- Shift Security Left: Catch vulnerabilities early in development
- Defense in Depth: Layer multiple security controls
- Principle of Least Privilege: Grant minimal necessary permissions
- Automate Security: Make security checks part of CI/CD
- Monitor Continuously: Detect and respond to threats in real-time
- Stay Updated: Regularly patch and update dependencies
Start with the basics—minimal images, non-root users, and vulnerability scanning—then progressively add advanced controls like admission controllers, runtime security monitoring, and comprehensive audit logging.
Need help implementing container security? InstaDevOps provides expert consulting and implementation for container security, Kubernetes hardening, and compliance. Contact us for a free consultation.
Need Help with Your DevOps Infrastructure?
At InstaDevOps, we specialize in helping startups and scale-ups build production-ready infrastructure without the overhead of a full-time DevOps team.
Our Services:
- 🏗️ AWS Consulting - Cloud architecture, cost optimization, and migration
- ☸️ Kubernetes Management - Production-ready clusters and orchestration
- 🚀 CI/CD Pipelines - Automated deployment pipelines that just work
- 📊 Monitoring & Observability - See what's happening in your infrastructure
Special Offer: Get a free DevOps audit - 50+ point checklist covering security, performance, and cost optimization.
📅 Book a Free 15-Min Consultation
Originally published at instadevops.com
Top comments (0)