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
Warning: Setting cert-expiry too low (under 1 hour) can cause issues if clock skew exists between nodes or if a node is temporarily unreachable during renewal. 24 hours is aggressive but safe for most environments.

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
Tip: Most applications need a writable /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
Warning: 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
Tip: usulnet includes built-in security scanning for container images, showing CVE reports alongside your running services. This makes it straightforward to identify which containers in your Swarm cluster are running images with known vulnerabilities and need updating.

Security Hardening Checklist

  1. Certificate rotation: Set --cert-expiry to 30 days or less
  2. Rotate join tokens: After any security incident or personnel change
  3. Encrypt overlay networks: Enable --opt encrypted for all sensitive traffic
  4. Network segmentation: Use internal overlay networks for database traffic
  5. Secrets over environment variables: Always use Swarm secrets for credentials
  6. Read-only filesystems: Enable read_only: true where possible
  7. Drop capabilities: Use cap_drop: [ALL] and selectively add back only what is needed
  8. No privileged containers: Never run containers in privileged mode in production
  9. User namespaces: Enable userns-remap if your workloads support it
  10. Image scanning: Scan all images before deployment; block images with critical CVEs
  11. Audit logging: Enable auditd rules for Docker and Swarm operations
  12. Docker socket protection: Never mount /var/run/docker.sock into 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.