Skip to main content
Exposed ServiceCritical

Exposed Docker Engine API

An Exposed Docker Engine API is a Docker daemon listening on TCP port 2375 (or 2376 without client-certificate verification) where anyone who can reach the port controls the daemon. Because the daemon runs as root and can mount the host filesystem into a container, an unauthenticated request is equivalent to root-level remote code execution on the host.

A single misplaced `-H tcp://0.0.0.0:2375` flag turns a routine container host into an open root shell for the entire internet. The Docker Engine API was designed for a trusted local socket, not a public network port, yet it is repeatedly found bound to a routable interface with no TLS and no client authentication. The gap between how operators imagine this port is reached and how trivially an attacker reaches it is exactly the kind of blind spot that turns a development convenience into a full host takeover.

Reviewed by Ameya Lambat

Security Research Contributor, Legba

Reviewed 2026-05-28 · Updated 2026-05-28

What it is

The Docker Engine API is the HTTP control plane for the Docker daemon (`dockerd`). By default the daemon listens only on a local Unix socket (`/var/run/docker.sock`). When an operator adds a TCP listener (commonly `-H tcp://0.0.0.0:2375`) to enable remote management, CI integration, or orchestration, that same root-privileged API becomes reachable over the network. Port 2375 is the plaintext, unauthenticated variant; port 2376 is intended for TLS, but it is only safe when the daemon is started with `--tlsverify`, which forces mutual TLS and rejects clients that lack a CA-signed certificate. Without `--tlsverify`, even 2376 can accept anonymous commands. An exposed daemon answers basic, read-only-looking requests such as `GET /version` and `GET /containers/json`, but the same endpoint also accepts `POST /containers/create` and `POST /containers/{id}/start` — the calls that lead to compromise.

What an organization loses here is not data from one application; it is the entire host and everything that touches it. Because `dockerd` runs as root and the API accepts a container spec that bind-mounts the host root filesystem (`/`) into the container, an attacker can write a cron job, add an SSH key, or read `/etc/shadow` and cloud-instance credentials from the metadata-bearing filesystem — a host-takeover path that public container-security research has demonstrated end to end. In 2025, Docker assigned CVE-2025-9074 a CVSS 9.3 (Critical) precisely because a container could reach the Engine API at 192.168.65.7:2375 without authentication and pivot to the host. The practical consequences are cryptomining infections (the Doki and Kinsing campaigns specifically scanned for open Docker APIs), ransomware staging, lateral movement into the orchestration plane, and theft of every secret mounted into the containers that daemon manages. One reachable port can therefore cost an organization its production fleet, its build pipeline, and its cloud account in a single intrusion.

At a glance

Typeexposed-docker-api
Ports2375, 2376
Protocolstcp, http, https
Seen onDocker Engine, dockerd, Docker Desktop, Moby, Portainer, containerd, Kubernetes nodes running Docker
SeverityCritical
Updated2026-05-28

How attackers find and exploit it

  • Mass-scan the IPv4 space (or a target ASN) for TCP 2375 and 2376 using tools like masscan or Shodan/Censys saved queries, since both ports are static, well-known Docker defaults.
  • Confirm the service is Docker by sending an unauthenticated `GET /version` and parsing the JSON for fields such as `ApiVersion`, `Os`, and `GitCommit`; a 200 response with these keys fingerprints a live, talkable daemon.
  • Enumerate the environment with `GET /containers/json`, `GET /images/json`, and `GET /info` to learn running workloads, image names, mounted volumes, and host kernel details that inform the next step.
  • Pull or reference a small image already present on the host, then `POST /containers/create` with a HostConfig binds entry such as `/:/host` (or `Privileged: true`) to map the host root filesystem into a new container.
  • Start the container with `POST /containers/{id}/start` and run a command through the entrypoint that chroots into `/host` to write an SSH authorized_keys entry, drop a cron job, or read `/etc/shadow` and cloud credential files — achieving root-level code execution on the host.
  • Establish persistence and pivot: deploy a coin miner or reverse shell, harvest secrets and registry credentials, and reuse the Docker API to start additional containers on the same or adjacent hosts.

How to detect it on your surface

  • Inventory every internet-facing and internal IP for open TCP 2375 and 2376 from an out-of-network vantage point, since internal-only firewall rules frequently fail to cover cloud security-group or VPC-peering paths.
  • On each host, inspect the daemon launch configuration — `/etc/docker/daemon.json` for a `hosts` array and the systemd unit or `dockerd` command line for `-H tcp://` flags — to find TCP listeners that should not exist.
  • Confirm whether any TCP listener enforces mutual TLS by checking for `--tlsverify`, `--tlscacert`, `--tlscert`, and `--tlskey`; a TCP listener without `--tlsverify` is exposed even on port 2376.
  • Review cloud security groups, network ACLs, and load-balancer rules for any allow rule permitting 2375/2376 from `0.0.0.0/0` or broad internal CIDRs.
  • Search CI/CD configuration and infrastructure-as-code (Terraform, Ansible, Helm) for templated `DOCKER_HOST=tcp://...:2375` values that provision exposed daemons at scale.

Detection signals

  • An unauthenticated `GET /version` returns HTTP 200 with a JSON body containing `ApiVersion`, `Version`, `GitCommit`, `Os`, and `Arch` fields — the canonical Docker daemon fingerprint.
  • `GET /containers/json` returns HTTP 200 with a JSON array of container objects (each with `Id`, `Image`, `Command`, `Ports`, `State`) rather than a 401/403, proving no authentication is enforced.
  • The HTTP response header advertises `Server: Docker/<version> (linux)` or `Api-Version:` on responses from the port.
  • TCP 2375 responds in cleartext HTTP (no TLS handshake), or 2376 completes a TLS handshake but still answers API calls without requesting a client certificate, indicating `--tlsverify` is absent.
  • `GET /info` exposes host-level details such as `KernelVersion`, `OperatingSystem`, `Architecture`, `NCPU`, and `DockerRootDir`, confirming a fully readable daemon.

Validate before you report

  • Issue a non-destructive `GET /version` and verify a 200 with valid Docker JSON before asserting the service is a real daemon (not a honeypot or unrelated service squatting on the port).
  • Send `GET /_ping` and confirm the response body is the literal `OK` and the `Api-Version` header is present, which only a genuine Docker daemon returns.
  • Call `GET /containers/json` and `GET /images/json` to confirm the API answers privileged enumeration without credentials — if these return data, authentication is definitively absent.
  • Without creating or starting any container, inspect `GET /info` for `Swarm`, `SecurityOptions`, and `DockerRootDir` to characterize exploitability (for example, whether userns-remap or rootless mode is in effect) and to capture evidence of the configuration.
  • Record the exact request/response pairs (port, headers, JSON snippets) as proof so the finding is an evidenced true positive rather than a port-scan guess, and explicitly do not run `POST /containers/create` against production targets.

What looks like this but isn't

  • A reverse proxy or API gateway in front of 2375/2376 that terminates requests with 401/403 or mutual-TLS challenge — verify the daemon itself, not the edge, is what answers `GET /version`.
  • A daemon started with `--tlsverify` on 2376 that completes the TLS handshake but rejects clients lacking a CA-signed client certificate; this is authenticated and is not the exposure.
  • An unrelated service or honeypot bound to 2375/2376 that does not return valid Docker JSON for `/version` and `/_ping` — absence of the `Api-Version` header and proper body indicates a false match.
  • Internal-only listeners reachable solely over a Unix socket or a host-restricted `127.0.0.1` bind that a misconfigured scanner reports as open due to SSH tunneling or port-forwarding rather than a true network exposure.

Remediation

  • Stop binding the daemon to a network interface: remove any `-H tcp://...` flag and any `hosts` TCP entry from `/etc/docker/daemon.json` and the systemd unit, leaving only the local Unix socket, then restart `dockerd`.
  • If remote management is genuinely required, enable mutual TLS by starting the daemon with `--tlsverify`, `--tlscacert`, `--tlscert`, and `--tlskey`, and distribute client certificates signed by your private CA so only verified clients connect on 2376.
  • Prefer the SSH transport (`DOCKER_HOST=ssh://user@host`) or an SSH tunnel over exposing any TCP port, so authentication and encryption ride on hardened SSH rather than a bespoke TLS setup.
  • Restrict the port at the network layer regardless of TLS: deny 2375/2376 from `0.0.0.0/0` in cloud security groups, host firewalls (iptables/ufw), and network ACLs, allowing only specific management source IPs.
  • Rotate any secrets, SSH keys, registry credentials, and cloud-instance credentials that were reachable from the host while the API was open, since exposure means assume-breach for everything the daemon could touch.
  • Harden the daemon itself: run rootless Docker or enable user-namespace remapping (`userns-remap`), disable inter-container communication where unneeded, and forbid `--privileged` and host bind-mounts in your container-creation policy.

Operational checklist

  • Continuously scan external and internal ranges for open 2375/2376 and alert on any new listener so a re-introduced exposure is caught within hours, not at the next pentest.
  • Codify the daemon configuration in infrastructure-as-code with no TCP `hosts` entry, and add a policy check (OPA/Conftest or a CI lint) that fails any build provisioning `tcp://...:2375`.
  • Require `--tlsverify` with mutual TLS on every Docker host that must accept remote connections, and audit certificate expiry and revocation on a schedule.
  • Enforce least privilege on the daemon: rootless mode or userns-remap as the default baseline, with `--privileged` and host-path bind mounts gated behind explicit approval.
  • Centralize Docker daemon and API access logs, and alert on `containers/create` or `containers/start` calls originating from unexpected source addresses.
  • Include Docker API exposure in quarterly attack-surface reviews and validate that cloud security-group changes have not silently re-opened 2375/2376.

What to do next

An open Docker Engine API is not a low-priority hygiene item — it is a root shell on your host waiting for the first scanner to find it, and the campaigns that weaponize it (Doki, Kinsing, and the technique behind CVE-2025-9074) are automated and continuous. The job your team actually needs done is not another scan result that says "port 2375 open"; it is a confirmed, evidenced finding that proves the daemon answered an unauthenticated request, so you can justify the change and close it today. Pull your daemon configs now, kill the TCP listener, lock the port at the network edge, and rotate the secrets that host could reach. Treat any exposure window as a breach until proven otherwise.

Methodology

Each finding-type guide is built from Legba Recon's real detection and validation logic, reviewed by a named security contributor, and cited against primary sources such as OWASP, CISA, NIST, and MITRE. We update pages when the underlying guidance changes. See our contributors and company.

FAQs.

References.

Weakness references (CWE)

Keep exploring

Your agent needs its Legba.

Read the docs