Docker Container Security: Best Practices and Common Pitfalls
Comprehensive guide to securing Docker containers, from image hardening to runtime security monitoring.
8 min read
ibrahimsql
1,416 words
Docker Container Security: Best Practices and Common Pitfalls#
As containerization becomes the standard for application deployment, securing Docker containers has become a critical skill for cybersecurity professionals. This guide covers essential security practices and common vulnerabilities.
Container Security Fundamentals#
The Container Attack Surface#
Containers introduce several attack vectors:
- Container Images: Vulnerable base images and packages
- Container Runtime: Docker daemon and runtime vulnerabilities
- Host System: Kernel and host OS security
- Orchestration: Kubernetes and container orchestration security
- Network: Container-to-container and external communications
Image Security#
Base Image Selection#
# BAD: Using latest tag and full OS FROM ubuntu:latest # GOOD: Using specific version and minimal base FROM alpine:3.18.4 # or even better FROM scratch
Multi-Stage Builds#
# Multi-stage build for security and size optimization FROM golang:1.21-alpine AS builder WORKDIR /app COPY . . RUN go build -o myapp # Final stage with minimal image FROM alpine:3.18.4 RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY /app/myapp . USER 1000:1000 CMD ["./myapp"]
Image Scanning#
# Using Trivy for vulnerability scanning trivy image nginx:latest # Using Docker Scout docker scout cves nginx:latest # Using Clair clairctl analyze nginx:latest
Runtime Security#
User and Privilege Management#
# Create non-root user RUN addgroup -g 1001 appgroup && \ adduser -D -u 1001 -G appgroup appuser # Switch to non-root user USER appuser # Set proper file permissions COPY app.jar /app/
Security Contexts#
# Kubernetes security context apiVersion: v1 kind: Pod spec: securityContext: runAsNonRoot: true runAsUser: 1001 fsGroup: 1001 containers: - name: app securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL add: - NET_BIND_SERVICE
Resource Limits#
# Docker Compose resource limits services: app: image: myapp:latest deploy: resources: limits: cpus: '0.5' memory: 512M reservations: cpus: '0.25' memory: 256M
Network Security#
Network Segmentation#
# Create custom networks docker network create --driver bridge app-network docker network create --driver bridge db-network # Run containers in specific networks docker run --network app-network myapp docker run --network db-network postgres
Network Policies (Kubernetes)#
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-all spec: podSelector: {} policyTypes: - Ingress - Egress --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-app-to-db spec: podSelector: matchLabels: app: database policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: web ports: - protocol: TCP port: 5432
Secrets Management#
Docker Secrets#
# Create a secret echo "mysecretpassword" | docker secret create db_password - # Use secret in service docker service create \ --name myapp \ --secret db_password \ myapp:latest
Kubernetes Secrets#
apiVersion: v1 kind: Secret metadata: name: app-secrets type: Opaque data: database-password: <base64-encoded-password> --- apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: app env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secrets key: database-password
Security Scanning and Monitoring#
Automated Security Scanning#
#!/usr/bin/env python3 """ Automated Docker security scanner """ import docker import subprocess import json def scan_image(image_name): """ Scan Docker image for vulnerabilities """ results = {} # Trivy scan try: trivy_result = subprocess.run( ['trivy', 'image', '--format', 'json', image_name], capture_output=True, text=True ) results['trivy'] = json.loads(trivy_result.stdout) except Exception as e: results['trivy_error'] = str(e) # Docker Scout scan try: scout_result = subprocess.run( ['docker', 'scout', 'cves', '--format', 'json', image_name], capture_output=True, text=True ) results['scout'] = json.loads(scout_result.stdout) except Exception as e: results['scout_error'] = str(e) return results def check_container_config(container_id): """ Check container configuration for security issues """ client = docker.from_env() container = client.containers.get(container_id) issues = [] # Check if running as root if container.attrs['Config']['User'] == '' or container.attrs['Config']['User'] == 'root': issues.append("Container running as root") # Check for privileged mode if container.attrs['HostConfig']['Privileged']: issues.append("Container running in privileged mode") # Check for host network if container.attrs['HostConfig']['NetworkMode'] == 'host': issues.append("Container using host network") return issues
Runtime Monitoring#
# Using Falco for runtime security monitoring # Install Falco curl -s https://falco.org/repo/falcosecurity-3672BA8F.asc | apt-key add - echo "deb https://download.falco.org/packages/deb stable main" | tee -a /etc/apt/sources.list.d/falcosecurity.list apt-get update -y apt-get install -y falco # Custom Falco rule for container security cat > /etc/falco/rules.d/container_security.yaml << EOF - rule: Container Running as Root desc: Detect containers running as root condition: container and user.uid=0 output: Container running as root (user=%user.name container=%container.name) priority: WARNING - rule: Sensitive File Access desc: Detect access to sensitive files condition: open_read and fd.name in (/etc/passwd, /etc/shadow, /etc/hosts) output: Sensitive file accessed (file=%fd.name container=%container.name) priority: HIGH EOF
Container Escape Prevention#
AppArmor/SELinux Profiles#
# AppArmor profile for Docker cat > /etc/apparmor.d/docker-security << EOF #include <tunables/global> profile docker-security flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base> # Deny dangerous capabilities deny capability sys_admin, deny capability sys_module, deny capability sys_rawio, # Allow necessary file access /usr/bin/** ix, /lib/** mr, /etc/** r, # Deny sensitive file access deny /proc/sys/** w, deny /sys/** w, } EOF # Load the profile apparmor_parser -r /etc/apparmor.d/docker-security # Run container with profile docker run --security-opt apparmor=docker-security myapp
Seccomp Profiles#
{ "defaultAction": "SCMP_ACT_ERRNO", "architectures": [ "SCMP_ARCH_X86_64" ], "syscalls": [ { "names": [ "read", "write", "open", "close", "stat", "fstat", "lstat", "poll", "lseek", "mmap", "mprotect", "munmap", "brk", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "ioctl", "pread64", "pwrite64", "readv", "writev", "access", "pipe", "select", "sched_yield", "mremap", "msync", "mincore", "madvise", "shmget", "shmat", "shmctl", "dup", "dup2", "pause", "nanosleep", "getitimer", "alarm", "setitimer", "getpid", "sendfile", "socket", "connect", "accept", "sendto", "recvfrom", "sendmsg", "recvmsg", "shutdown", "bind", "listen", "getsockname", "getpeername", "socketpair", "setsockopt", "getsockopt", "clone", "fork", "vfork", "execve", "exit", "wait4", "kill", "uname", "semget", "semop", "semctl", "shmdt", "msgget", "msgsnd", "msgrcv", "msgctl", "fcntl", "flock", "fsync", "fdatasync", "truncate", "ftruncate", "getdents", "getcwd", "chdir", "fchdir", "rename", "mkdir", "rmdir", "creat", "link", "unlink", "symlink", "readlink", "chmod", "fchmod", "chown", "fchown", "lchown", "umask", "gettimeofday", "getrlimit", "getrusage", "sysinfo", "times", "ptrace", "getuid", "syslog", "getgid", "setuid", "setgid", "geteuid", "getegid", "setpgid", "getppid", "getpgrp", "setsid", "setreuid", "setregid", "getgroups", "setgroups", "setresuid", "getresuid", "setresgid", "getresgid", "getpgid", "setfsuid", "setfsgid", "getsid", "capget", "capset", "rt_sigpending", "rt_sigtimedwait", "rt_sigqueueinfo", "rt_sigsuspend", "sigaltstack", "utime", "mknod", "uselib", "personality", "ustat", "statfs", "fstatfs", "sysfs", "getpriority", "setpriority", "sched_setparam", "sched_getparam", "sched_setscheduler", "sched_getscheduler", "sched_get_priority_max", "sched_get_priority_min", "sched_rr_get_interval", "mlock", "munlock", "mlockall", "munlockall", "vhangup", "modify_ldt", "pivot_root", "_sysctl", "prctl", "arch_prctl", "adjtimex", "setrlimit", "chroot", "sync", "acct", "settimeofday", "mount", "umount2", "swapon", "swapoff", "reboot", "sethostname", "setdomainname", "iopl", "ioperm", "create_module", "init_module", "delete_module", "get_kernel_syms", "query_module", "quotactl", "nfsservctl", "getpmsg", "putpmsg", "afs_syscall", "tuxcall", "security", "gettid", "readahead", "setxattr", "lsetxattr", "fsetxattr", "getxattr", "lgetxattr", "fgetxattr", "listxattr", "llistxattr", "flistxattr", "removexattr", "lremovexattr", "fremovexattr", "tkill", "time", "futex", "sched_setaffinity", "sched_getaffinity", "set_thread_area", "io_setup", "io_destroy", "io_getevents", "io_submit", "io_cancel", "get_thread_area", "lookup_dcookie", "epoll_create", "epoll_ctl_old", "epoll_wait_old", "remap_file_pages", "getdents64", "set_tid_address", "restart_syscall", "semtimedop", "fadvise64", "timer_create", "timer_settime", "timer_gettime", "timer_getoverrun", "timer_delete", "clock_settime", "clock_gettime", "clock_getres", "clock_nanosleep", "exit_group", "epoll_wait", "epoll_ctl", "tgkill", "utimes", "vserver", "mbind", "set_mempolicy", "get_mempolicy", "mq_open", "mq_unlink", "mq_timedsend", "mq_timedreceive", "mq_notify", "mq_getsetattr", "kexec_load", "waitid", "add_key", "request_key", "keyctl", "ioprio_set", "ioprio_get", "inotify_init", "inotify_add_watch", "inotify_rm_watch", "migrate_pages", "openat", "mkdirat", "mknodat", "fchownat", "futimesat", "newfstatat", "unlinkat", "renameat", "linkat", "symlinkat", "readlinkat", "fchmodat", "faccessat", "pselect6", "ppoll", "unshare", "set_robust_list", "get_robust_list", "splice", "tee", "sync_file_range", "vmsplice", "move_pages", "utimensat", "epoll_pwait", "signalfd", "timerfd_create", "eventfd", "fallocate", "timerfd_settime", "timerfd_gettime", "accept4", "signalfd4", "eventfd2", "epoll_create1", "dup3", "pipe2", "inotify_init1", "preadv", "pwritev", "rt_tgsigqueueinfo", "perf_event_open", "recvmmsg", "fanotify_init", "fanotify_mark", "prlimit64", "name_to_handle_at", "open_by_handle_at", "clock_adjtime", "syncfs", "sendmmsg", "setns", "getcpu", "process_vm_readv", "process_vm_writev", "kcmp", "finit_module", "sched_setattr", "sched_getattr", "renameat2", "seccomp", "getrandom", "memfd_create", "kexec_file_load", "bpf", "execveat", "userfaultfd", "membarrier", "mlock2", "copy_file_range", "preadv2", "pwritev2" ], "action": "SCMP_ACT_ALLOW" } ] }
CI/CD Security Integration#
GitLab CI Security Pipeline#
# .gitlab-ci.yml stages: - build - security-scan - deploy variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "/certs" build: stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA container-scanning: stage: security-scan image: docker:stable services: - docker:dind script: - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy image --exit-code 1 --severity HIGH,CRITICAL \ $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA allow_failure: false secret-detection: stage: security-scan image: trufflesecurity/trufflehog:latest script: - trufflehog git file://. --only-verified allow_failure: false deploy: stage: deploy script: - kubectl apply -f k8s/ only: - main
Incident Response#
Container Forensics#
#!/bin/bash # Container incident response script CONTAINER_ID=$1 OUTPUT_DIR="/tmp/container-forensics-$(date +%Y%m%d-%H%M%S)" mkdir -p $OUTPUT_DIR # Collect container information docker inspect $CONTAINER_ID > $OUTPUT_DIR/container-inspect.json docker logs $CONTAINER_ID > $OUTPUT_DIR/container-logs.txt docker exec $CONTAINER_ID ps aux > $OUTPUT_DIR/processes.txt docker exec $CONTAINER_ID netstat -tulpn > $OUTPUT_DIR/network.txt # Collect file system changes docker diff $CONTAINER_ID > $OUTPUT_DIR/filesystem-changes.txt # Export container filesystem docker export $CONTAINER_ID > $OUTPUT_DIR/container-filesystem.tar # Collect host information ps aux | grep docker > $OUTPUT_DIR/host-docker-processes.txt netstat -tulpn | grep docker > $OUTPUT_DIR/host-docker-network.txt echo "Forensics data collected in $OUTPUT_DIR"
Conclusion#
Container security requires a multi-layered approach covering the entire container lifecycle. Key takeaways:
- Secure by Design: Build security into your containers from the start
- Least Privilege: Run containers with minimal permissions
- Defense in Depth: Use multiple security controls
- Continuous Monitoring: Implement runtime security monitoring
- Regular Updates: Keep base images and dependencies updated
Stay vigilant and keep learning as container security continues to evolve!