Securing Docker Swarm: Hardening Your Cluster Against Threats
Docker Swarm has a significant security advantage over manually orchestrated Docker hosts: it ships with mutual TLS authentication, encrypted communication between nodes, and a built-in secret management system. But these defaults are just the foundation. A production Swarm cluster faces threats from compromised containers, lateral movement between services, unauthorized access to the Docker socket, and supply chain attacks through container images.
This guide covers a defense-in-depth approach to Swarm security: hardening the cluster infrastructure, restricting what containers can do, encrypting everything in transit, and establishing audit trails that let you detect and investigate incidents.
Swarm's Built-In Security Model
When you initialize a Swarm with docker swarm init, Docker automatically provisions a complete PKI infrastructure:
| Component | Purpose | Automatic |
|---|---|---|
| Root CA certificate | Trust anchor for all node certificates | Yes, generated on init |
| Node TLS certificates | Mutual authentication between nodes | Yes, issued on join |
| Certificate rotation | Automatic renewal before expiry | Yes, default 90-day rotation |
| Encrypted Raft log | At-rest encryption for cluster state | Yes, AES-256-GCM |
| Encrypted control plane | TLS for all manager-to-manager traffic | Yes |
| Join tokens | Authorization for node joining | Yes, separate tokens for manager/worker |
This is more than most orchestration platforms provide out of the box. However, the data plane (overlay network traffic between containers) is not encrypted by default, and the security of individual containers depends on how they are configured.
TLS Mutual Authentication
Every node in a Swarm cluster presents a TLS certificate to prove its identity. This mutual TLS (mTLS) ensures that only authorized nodes can communicate with the cluster and prevents man-in-the-middle attacks.
Using an External CA
For organizations with existing PKI infrastructure, you can initialize the Swarm with an external root CA:
# Generate a root CA (or use your existing one)
openssl genrsa -out ca-key.pem 4096
openssl req -x509 -new -nodes -key ca-key.pem \
-sha256 -days 3650 \
-subj "/CN=Swarm Root CA/O=MyOrg" \
-out ca-cert.pem
# Initialize Swarm with external CA
docker swarm init \
--external-ca protocol=cfssl,url=https://ca.internal:8888 \
--advertise-addr 10.0.1.10
Certificate Rotation
By default, Swarm rotates node certificates every 90 days. Shorten this for higher security:
# Set certificate rotation period to 24 hours
docker swarm update --cert-expiry 24h
# Check current certificate expiry setting
docker system info | grep "Expiry Duration"
# View a node's certificate details
openssl x509 -in /var/lib/docker/swarm/certificates/swarm-node.crt \
-text -noout | grep -A2 "Validity"
# Force immediate certificate rotation (all nodes)
docker swarm ca --rotate
Network Encryption
Swarm encrypts the control plane (manager-to-manager) by default, but data plane traffic (container-to-container across nodes) is unencrypted unless you explicitly enable it.
# Create encrypted overlay network
docker network create \
--driver overlay \
--opt encrypted \
secure-app-network
# In stack file
networks:
backend:
driver: overlay
driver_opts:
encrypted: "true"
database:
driver: overlay
driver_opts:
encrypted: "true"
internal: true # No outbound access
Network Segmentation
Follow the principle of least privilege for network access. Each service should only be able to communicate with the services it depends on:
version: "3.8"
services:
# Public-facing: only on frontend network
nginx:
image: nginx:latest
networks:
- frontend
ports:
- "443:443"
# API: bridges frontend and backend
api:
image: myapp/api:latest
networks:
- frontend
- backend
# Worker: bridges backend and database
worker:
image: myapp/worker:latest
networks:
- backend
- database
# Database: isolated internal network
postgres:
image: postgres:16
networks:
- database
networks:
frontend:
driver: overlay
backend:
driver: overlay
driver_opts:
encrypted: "true"
database:
driver: overlay
driver_opts:
encrypted: "true"
internal: true
In this topology, nginx cannot reach postgres because they share no network. A compromised nginx container would need to also compromise the API service before it could reach the database.
Secret Management
Swarm secrets are encrypted at rest in the Raft log and mounted as in-memory files (tmpfs) inside containers. They are the correct way to handle credentials.
# Create secrets
echo "strong-db-password" | docker secret create db_password -
docker secret create tls_cert ./server.crt
docker secret create tls_key ./server.key
# Use in services with restrictive permissions
docker service create \
--name api \
--secret source=db_password,target=db_password,mode=0400 \
--secret source=tls_cert,target=/certs/server.crt,mode=0444 \
--secret source=tls_key,target=/certs/server.key,mode=0400 \
myapp/api:latest
Never use environment variables for secrets. Environment variables are visible in
docker inspect, in/proc/<pid>/environ, and often leak into logs. Swarm secrets mounted as files are the only acceptable method for production credentials.
Node Labels for Sensitive Workloads
Use node labels to restrict where sensitive services can run. This ensures that, for example, services handling PII or financial data only run on nodes that meet specific compliance requirements:
# Label nodes by security classification
docker node update --label-add security=high secure-node-01
docker node update --label-add security=high secure-node-02
docker node update --label-add security=standard worker-01
docker node update --label-add security=standard worker-02
# Label by compliance zone
docker node update --label-add compliance=pci pci-node-01
docker node update --label-add compliance=hipaa hipaa-node-01
# Constrain sensitive services
docker service create \
--name payment-processor \
--constraint 'node.labels.compliance == pci' \
--secret stripe_key \
myapp/payments:latest
docker service create \
--name patient-records \
--constraint 'node.labels.compliance == hipaa' \
--secret db_password \
myapp/ehr:latest
Read-Only Containers
Running containers with a read-only root filesystem prevents attackers from writing malware, modifying binaries, or planting backdoors inside a container:
version: "3.8"
services:
api:
image: myapp/api:latest
read_only: true
tmpfs:
- /tmp:size=100M
- /var/run:size=10M
volumes:
- type: tmpfs
target: /app/cache
tmpfs:
size: 50M
deploy:
replicas: 4
# Via CLI
docker service create \
--name api \
--read-only \
--mount type=tmpfs,destination=/tmp,tmpfs-size=100M \
--mount type=tmpfs,destination=/var/run,tmpfs-size=10M \
myapp/api:latest
/tmp directory. Mount it as tmpfs to provide a writable scratch space without allowing persistent writes to the filesystem. This limits the damage an attacker can do even if they achieve code execution inside the container.
AppArmor Profiles
AppArmor is a Linux kernel security module that restricts what a process can do. Docker applies a default AppArmor profile to all containers, but you can create custom profiles for tighter restrictions:
# /etc/apparmor.d/docker-restricted
#include
profile docker-restricted flags=(attach_disconnected,mediate_deleted) {
#include
# Deny all network access except TCP
deny network udp,
deny network raw,
# Deny writes to sensitive paths
deny /etc/** w,
deny /usr/** w,
deny /bin/** w,
deny /sbin/** w,
# Allow reads from application directory
/app/** r,
/app/bin/* ix,
# Allow tmp writes
/tmp/** rw,
/var/run/** rw,
# Deny capabilities
deny capability sys_admin,
deny capability net_admin,
deny capability sys_ptrace,
}
# Load the profile
apparmor_parser -r /etc/apparmor.d/docker-restricted
# Use in a service
docker service create \
--name api \
--security-opt apparmor=docker-restricted \
myapp/api:latest
Seccomp Profiles
Seccomp (Secure Computing) filters restrict which system calls a container can make. Docker applies a default profile that blocks about 44 dangerous syscalls, but a custom profile can be much more restrictive:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"accept", "accept4", "bind", "brk", "chdir", "chmod",
"clock_gettime", "clone", "close", "connect", "dup", "dup2",
"epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait",
"execve", "exit", "exit_group", "fchmod", "fcntl", "fstat",
"futex", "getcwd", "getdents64", "getegid", "geteuid",
"getgid", "getpid", "getppid", "getsockname", "getsockopt",
"getuid", "ioctl", "listen", "lseek", "madvise", "mmap",
"mprotect", "munmap", "nanosleep", "newfstatat", "openat",
"pipe2", "poll", "pread64", "pwrite64", "read", "recvfrom",
"recvmsg", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn",
"sched_yield", "select", "sendmsg", "sendto", "set_robust_list",
"setsockopt", "shutdown", "socket", "stat", "tgkill",
"uname", "unlink", "write", "writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Use custom seccomp profile
docker service create \
--name api \
--security-opt seccomp=/path/to/seccomp-profile.json \
myapp/api:latest
Docker Daemon Hardening
The Docker daemon itself needs hardening on every Swarm node:
# /etc/docker/daemon.json
{
"icc": false,
"userns-remap": "default",
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true,
"storage-driver": "overlay2",
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 32768
},
"nproc": {
"Name": "nproc",
"Hard": 4096,
"Soft": 2048
}
}
}
| Setting | Purpose |
|---|---|
icc: false |
Disable inter-container communication on the default bridge (forces explicit network linking) |
userns-remap |
Run containers in user namespaces so root inside the container is not root on the host |
no-new-privileges |
Prevent processes from gaining additional privileges via setuid/setgid |
live-restore |
Keep containers running during Docker daemon upgrades |
userns-remap has compatibility issues with some Docker features, including Swarm mode in certain configurations. Test thoroughly before enabling in production. Also note that icc: false only affects the default bridge network; overlay networks use Swarm's network policies.
Audit Logging
Configure Linux audit rules to track Docker-related operations:
# /etc/audit/rules.d/docker.rules
# Monitor Docker daemon
-w /usr/bin/dockerd -k docker
-w /usr/bin/docker -k docker
# Monitor Docker config files
-w /etc/docker -k docker-config
-w /etc/docker/daemon.json -k docker-config
# Monitor Docker socket
-w /var/run/docker.sock -k docker-socket
# Monitor Swarm state
-w /var/lib/docker/swarm -k docker-swarm
# Monitor container creation
-a always,exit -F arch=b64 -S clone -S unshare -k container-create
# Reload audit rules
auditctl -R /etc/audit/rules.d/docker.rules
# Search audit logs for Docker events
ausearch -k docker --start recent
ausearch -k docker-socket --start today
Docker Events for Service Monitoring
# Stream all Swarm events
docker events --filter type=service
docker events --filter type=node
docker events --filter type=secret
# Log events to a file for analysis
docker events --format '{{json .}}' >> /var/log/docker-events.json &
# Forward to centralized logging
docker events --format '{{json .}}' | \
while read event; do
echo "$event" | logger -t docker-events -p local0.info
done
Image Security
Secure the images that run in your Swarm:
# Enable Docker Content Trust (image signing)
export DOCKER_CONTENT_TRUST=1
# Pull and verify signed images
docker pull myregistry.com/myapp:v2.1.0
# Sign images when pushing
docker trust sign myregistry.com/myapp:v2.1.0
# Scan images for vulnerabilities
docker scout cves myapp/api:latest
# or
trivy image myapp/api:latest
Registry Security
# Use a private registry with authentication
docker service create \
--name api \
--with-registry-auth \
myregistry.com/myapp/api:v2.1.0
Security Hardening Checklist
- Certificate rotation: Set
--cert-expiryto 30 days or less - Rotate join tokens: After any security incident or personnel change
- Encrypt overlay networks: Enable
--opt encryptedfor all sensitive traffic - Network segmentation: Use internal overlay networks for database traffic
- Secrets over environment variables: Always use Swarm secrets for credentials
- Read-only filesystems: Enable
read_only: truewhere possible - Drop capabilities: Use
cap_drop: [ALL]and selectively add back only what is needed - No privileged containers: Never run containers in privileged mode in production
- User namespaces: Enable
userns-remapif your workloads support it - Image scanning: Scan all images before deployment; block images with critical CVEs
- Audit logging: Enable auditd rules for Docker and Swarm operations
- Docker socket protection: Never mount
/var/run/docker.sockinto application containers
Conclusion
Docker Swarm's built-in security (mTLS, encrypted Raft, secret management) provides a solid baseline that many orchestration platforms require additional tools to match. Building on this foundation with encrypted overlays, network segmentation, restrictive container profiles, and audit logging creates a defense-in-depth posture that significantly raises the bar for attackers. Security is not a feature you enable once; it is an ongoing practice of auditing, scanning, and tightening the boundaries of what your containers can do.