guest@6admin.io:~$

Hardening Docker Swarm

Everything that applies to single-host Docker hardening still applies to each container in a Swarm, so this note only covers what the cluster adds: a control plane, a trust boundary between manager and worker, a network that spans hosts and a secrets store. The good news and it deserves saying because people look down on Swarm, is that its security defaults are better than assumed. Nodes authenticate each other over mutual TLS with rotating certificates, out of the box and the Raft store that holds the cluster state is encrypted at rest. The gaps are narrower and more precise: one network setting off by default, one operational habit that collapses the trust boundary and the usual temptation to put secrets in the wrong place.

The manager is the cluster

The single most important fact about a Swarm is that managers hold everything. A manager’s Raft log contains the cluster state, the service definitions, the join tokens and the secrets; a compromised manager is a compromised cluster, full stop. Two consequences follow, both operational rather than clever.

First, run no workload on the managers. A manager that also schedules application containers has merged the most sensitive node in the cluster with the most exposed one, so a single vulnerable app becomes a path to the Raft store. Drain them:

docker node update --availability drain manager1

Then keep the manager count small and odd, because Raft wants a quorum and every manager is one more copy of the crown jewels. Three managers tolerate one failure, five tolerate two and seven is the sensible ceiling; beyond that you buy more attack surface and slower consensus for resilience you will not use. Workers can be as many as you like, because a worker only holds what is scheduled onto it, not the store.

mTLS is a default worth keeping

When a node joins, Swarm issues it a certificate from the cluster CA and nodes authenticate every control-plane connection over mutual TLS, rotating the certificates automatically. It is a genuinely solid default and hardening is mostly about not undermining it: shorten the rotation interval if your threat model calls for it and protect the join tokens, which are the open sesame that lets a new node in.

docker swarm ca --rotate                      # rotate the cluster CA
docker swarm update --cert-expiry 48h         # shorter node-certificate lifetime
docker swarm join-token --rotate worker       # invalidate a leaked worker token

Join tokens are the equivalent of a cluster password and they leak like passwords, into shell history, CI logs, a wiki. The manager token in particular is catastrophic to lose, because it adds a full manager. Rotate them at the slightest suspicion and treat the manager token with the care its blast radius deserves.

Encrypt the overlay, because it is not encrypted by default

Services on an overlay network talk from one host to another and by default that traffic is not encrypted, only the control plane is. On an isolated network of trusted nodes that may be acceptable; on anything shared or routed, it is cleartext data on the wire. Encryption is a flag at network creation and it is the flag people forget:

docker network create --driver overlay --opt encrypted app-net

It adds IPSec between nodes at some CPU cost and it is the difference between a sniffer on the node network seeing your inter-service traffic and seeing noise. Segment while you are at it: a service attaches only to the overlay networks it needs, not to a single flat network that lets any compromised container reach every other.

Secrets: use the mechanism, not the environment

Swarm has a real secrets mechanism and the common failure is not using it. A Swarm secret is stored encrypted in Raft, delivered only to the services granted it and mounted into each task as a file under /run/secrets on a tmpfs that never touches disk:

printf 'db-pass' | docker secret create db_password -
docker service create --name app --secret db_password myimage:tag

The anti-patterns are the alternatives people reach for instead: a secret in an environment variable leaks through docker inspect, through the process environment any in-container RCE can read and often into logs; a secret baked into the image is in every registry copy forever. File-under-tmpfs delivery exists precisely to avoid that, so hardening is routing every credential through docker secret and granting it only to the services that use it. Rotation is a secret replacement plus a service update and old secrets must be removed, not left granted to tasks that no longer need them.

The rest is per container and per node

Beyond the cluster mechanics, a Swarm’s security is the sum of its containers and its hosts, so the single-host discipline carries over into the service definition: drop capabilities, set no-new-privileges, non-root user, read-only root, resource limits and never a privileged service or a Docker-socket mount, which in a Swarm deploys the escape to every node the service lands on rather than to one. A privileged service in --mode global is the cluster-wide version of the single-host mistake and worse in exact proportion to the node count.

Where to land

Keep workloads off the managers and the manager count small and odd, because the managers are the cluster and each is one more copy of the Raft store. Lean on the mTLS and encrypted-Raft defaults, rotate the join tokens and the CA and keep the manager token like the cluster password it is. Create overlay networks with --opt encrypted because the default is cleartext and attach each service only to the networks it needs. Deliver every credential through Swarm secrets so it lives encrypted in Raft and arrives as a tmpfs file, never as an environment variable. Then apply ordinary container hardening to each service, remembering that in a cluster a privileged misstep is multiplied by the number of nodes it runs on.

[edit: 2026] The Ansible implementation of all this: the docker role .