Remediation Guides

Docker Remote API 2375 Exposed: How to Fix Nessus 124029

26 September 2026 8 min read

Docker Remote API Detection (Nessus plugin 124029) means dockerd answers Engine API requests over TCP without authentication, typically as the plaintext Docker Remote API 2375 listener. Anyone who can reach it gets root-level control of the host. To fix it, remove the tcp:// listener from dockerd and use SSH, or mutual TLS on port 2376, for remote access.

What the scanner is actually detecting

Two tools commonly raise this: Nessus probes the network, docker-bench-security reads the local daemon configuration.

Scanner ID and title What it matches
Tenable Nessus Plugin 124029: Docker Remote API Detection, severity Critical, family Service detection A remote check that reports when the Docker Engine API answers without authentication. Tenable lists its required ports as services Nessus identified as web servers plus 2376, so it is not limited to 2375. It is scored CVSS v3 10.0.
docker-bench-security 2.7 – Ensure TLS authentication for Docker daemon is configured (Scored) A local check. It warns when /etc/docker/daemon.json has a hosts entry with tcp://, or the running dockerd has a -H flag other than unix:// or fd://, and TLS client verification is off. It passes only with tlsverify.

Real-world risk

The Engine API has no permission model of its own, and dockerd runs as root. Docker’s security documentation explains that the API can start a container with the host’s root directory mounted and change the host filesystem without restriction. On a plaintext 2375 listener there is no login, so the only control is who can reach the port.

In 2019 Palo Alto Networks Unit 42 described Graboid, a worm that spread through unauthenticated Docker APIs and ran cryptocurrency miners in containers. Treat an internet-facing listener as a possible incident: close it, then look for containers, images and volumes nobody recognizes.

Internal-only exposure is less urgent but still serious: any compromised machine on that network can become root on the host. Docker also warns that a firewall limiting other hosts does not stop containers on the same host from reaching the endpoint. If the host is internet-facing, make sure it sits inside your external attack surface management scope so the next exposed port is found by you first.

How to confirm it on the host

Search by port, not only by process name, because a TCP socket created through systemd socket activation is owned by systemd rather than dockerd:

sudo ss -ltnp '( sport = :2375 or sport = :2376 )'
ps -o args= -C dockerd
docker info 2>&1 | grep -i 'API is accessible'

Current Docker Engine releases add a deprecation notice to docker info for every TCP listener that lacks TLS client verification. Next, find where the listener is configured. Check all three places:

systemctl cat docker.service      # unit plus drop-ins in /etc/systemd/system/docker.service.d/
systemctl cat docker.socket       # a ListenStream= line with an IP or port
sudo cat /etc/docker/daemon.json  # "hosts", "tls" and "tlsverify" keys

systemctl cat also shows any EnvironmentFile= the unit reads, which some distribution packages use for extra dockerd flags. From the scanner’s network, confirm what Nessus saw:

curl -s --max-time 5 http://docker01.example.com:2375/version
nmap -sV -p 2375,2376 docker01.example.com

If curl returns a JSON document with an ApiVersion field, the API is open without authentication.

How to fix it

Back up what you are about to change:

sudo cp -a /etc/docker/daemon.json /etc/docker/daemon.json.bak
sudo cp -a /etc/systemd/system/docker.service.d /root/docker.service.d.bak

Option 1: remove the TCP listener (recommended)

If the port is set in a systemd drop-in, edit that drop-in so ExecStart keeps only the stock socket. The empty ExecStart= line is required because it clears the vendor value:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Keep any other flags your package’s own unit uses. If the drop-in existed only for the TCP port, deleting it is cleaner. Avoid systemctl revert docker.service unless you intend to drop every drop-in, including proxy settings.

If the port is set in daemon.json, remove the tcp:// entry from hosts:

{
  "hosts": ["unix:///var/run/docker.sock"]
}

This is where the duplicate -H trap bites. The stock systemd unit already passes -H fd://, and dockerd refuses to start when an option comes from both a flag and the file, logging the following directives are specified both as a flag and in the configuration file: hosts. A working hosts key therefore means someone also overrode ExecStart without -H. Either keep both with only the Unix socket listed, or delete the hosts key and that override together, never just one.

Validate and restart:

sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo systemctl daemon-reload
sudo systemctl restart docker

If docker.socket carries the TCP port (a drop-in adding something like ListenStream=0.0.0.0:2375), remove that line, keep the Unix socket, then run sudo systemctl daemon-reload, sudo systemctl stop docker.service docker.socket and sudo systemctl start docker.socket docker.service.

Option 2: remote access over SSH

Docker’s documentation names SSH as the alternative when TLS is not practical. It opens no Docker port at all:

docker context create --docker host=ssh://docker-user@docker01.example.com prod-docker
docker context use prod-docker
docker info

# or, for one shell session
export DOCKER_HOST=ssh://docker-user@docker01.example.com

The client needs SSH public key authentication (password login is not supported, and passphrase-protected keys need ssh-agent). The remote user must be able to use the Docker socket, which is root-equivalent, so limit who holds that account.

Option 3: mutual TLS on port 2376

For tools that only speak tcp://, follow Docker’s “Protect the Docker daemon socket” guide to create a CA, a server certificate whose subjectAltName covers every DNS name and IP clients use, and client certificates with extendedKeyUsage = clientAuth. Bind to a management IP, not 0.0.0.0, in the drop-in:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://10.0.5.20:2376 --containerd=/run/containerd/containerd.sock

Put the TLS settings in /etc/docker/daemon.json only, so nothing is duplicated as a flag:

{
  "tlsverify": true,
  "tlscacert": "/etc/docker/tls/ca.pem",
  "tlscert": "/etc/docker/tls/server-cert.pem",
  "tlskey": "/etc/docker/tls/server-key.pem"
}

Set private keys to mode 0400. tlsverify is what matters: tls alone encrypts traffic but accepts any client. Clients connect with:

docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem 
  -H=tcp://docker01.example.com:2376 version

# or, with ca.pem, cert.pem and key.pem copied to ~/.docker
export DOCKER_HOST=tcp://docker01.example.com:2376 DOCKER_TLS_VERIFY=1

Guard client keys like a root password; whoever holds one controls the host.

Restrict the port with a host firewall

# ufw, with the default incoming policy set to deny
sudo ufw allow from 10.0.5.0/24 to any port 2376 proto tcp

# firewalld
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.5.0/24" port port="2376" protocol="tcp" accept'
sudo firewall-cmd --reload

This is defense in depth, not a substitute for TLS.

How to verify the fix and rescan

  1. sudo ss -ltnp ‘( sport = :2375 or sport = :2376 )’ prints nothing, or only 2376 on the management IP.
  2. docker info 2>&1 | grep -i ‘API is accessible’ prints nothing.
  3. From another host, curl -s –max-time 5 http://docker01.example.com:2375/version fails, and for TLS, curl -sk https://docker01.example.com:2376/version without a client certificate returns no JSON, while your SSH context or TLS client still works.
  4. sudo sh docker-bench-security.sh -c check_2_7 reports INFO (not listening on TCP) or PASS. docker-bench only reads dockerd flags and daemon.json, so it misses a TCP ListenStream in docker.socket; the ss check catches it.
  5. Rescan with Nessus using a port range that includes 2375 and 2376, or all ports. A scan that never probes the port proves nothing. Plugin 124029 should no longer appear.

What can break and how to roll back

  • Remote clients that used tcp://docker01.example.com:2375 (CI runners, build agents, management UIs, scripts with DOCKER_HOST set) fail with “Cannot connect to the Docker daemon”. Identify them first by watching established connections for a few days with ss -tn ‘( sport = :2375 )’. A CI runner is also a good moment to revisit how you scan Docker images in CI.
  • Docker will not start, almost always because of the hosts conflict above. journalctl -u docker -n 50 names the conflicting directive.
  • Running containers stop when the daemon restarts unless live-restore is enabled; containers with a restart policy come back according to it. Use a maintenance window.
  • TLS certificates expire (Docker’s example uses 365 days) and break every client at once, so track expiry.

To roll back, restore the backups, then run sudo systemctl daemon-reload and sudo systemctl restart docker. If TCP must return temporarily, bind it to 127.0.0.1 or a firewalled management IP rather than 0.0.0.0, and record a time-limited exception.

Common false positive reasons

  • Stale scan data. The report predates the fix.
  • Loopback-only binding. docker-bench 2.7 warns on any tcp:// host, including tcp://127.0.0.1:2375, which Nessus cannot see from the network. Lower risk, not zero.
  • The API belongs to a container. A Docker-in-Docker or similar container that publishes its own daemon port answers for the host IP. Check docker ps –filter publish=2375. The exposure is real, but the fix is removing that published port.
  • NAT or load balancer. The reported IP forwards the port to a different backend, so the host in your inventory may be clean while the real daemon runs elsewhere.
  • TLS without client verification is not a false positive: anyone reaching the port can still use the API.

FAQ

Is binding the API to 127.0.0.1:2375 an acceptable fix?

It removes the network exposure, but docker-bench still warns and any local process can call the API without docker group membership. Dockerd itself warns that localhost binding can be reached by browser scripts. Prefer the Unix socket plus SSH.

Does enabling TLS without tlsverify fix Nessus 124029?

No. The tls option encrypts traffic but does not authenticate clients, and docker-bench keeps warning in that mode. Only tlsverify with tlscacert requires a client certificate signed by your CA.

Why is a detection plugin rated Critical?

Because the detection is the vulnerability. Tenable scores it CVSS 10.0 with the rationale “Unauthenticated Administrative Access”. No software flaw is needed; the API works as designed.

Is DOCKER_HOST=ssh:// as safe as TLS?

It relies on your existing SSH controls instead of a separate CA and opens no new port. Either way, whoever holds the key gets root-equivalent access.

Tracking this finding across many hosts

An open 2375 port tends to return when a host is rebuilt from an old image. SITEY is one option for tracking it: it imports findings from 16 scanners (Nessus results by uploading an exported .nessus file), merges duplicates per scanner rather than across scanners, and uses AI triage to suggest false positives with evidence while a human decides. Host-specific fix scripts run only after human approval through SITEY agents on Linux endpoints, and per-finding retest is available for Nessus findings such as this one.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing