GET /containers/json Requests in Your Logs: Docker Engine API Scanning
Published
An automated probe for the Docker Engine API's container-listing endpoint, which on a daemon reachable over the network answers without any authentication and gives the caller control of the host.
What it looks like
203.0.113.7 - - [18/Oct/2026:03:51:11 +0000] "GET /containers/json HTTP/1.1" 404 162 "-" "Go-http-client/1.1" 198.51.100.42 - - [18/Oct/2026:03:51:12 +0000] "GET /v1.40/containers/json HTTP/1.1" 404 162 "-" "python-requests/2.31.0" 192.0.2.88 - - [18/Oct/2026:04:12:03 +0000] "GET /containers/json HTTP/1.1" 404 162 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
On our own server, the 30 days to 18 October 2026
72
requests · about 2.4 a day
60
source addresses
| Most requested variants | Requests |
|---|---|
| /containers/json | 70 |
| /v1.40/containers/json | 2 |
LogTriage's production API server (Hetzner, Germany): port 22 open to the internet, key-only since 6 October 2026 (password and keyboard-interactive authentication off); nginx in front of the API on ports 80 and 443. Paths as requested, query strings dropped and identifiers redacted; counted weekly from the nginx access log. More in the monthly attack report.
What the request is
GET /containers/json is a Docker Engine API call. It returns a JSON array describing containers on the daemon — by default the running ones, with image names, container names, command lines, labels, published ports and network details. /images/json, counted by the same pattern, is its sibling for images. The /v1.40/ prefix pins a specific API version; the unversioned form lets the daemon use its current one, and scanners try both because either may be accepted. Both variants show up in these logs.
The daemon normally listens on a Unix socket, where access is controlled by filesystem permissions. It can instead be told to listen on TCP, conventionally port 2375 without TLS and 2376 with it. The API itself carries no login step: whoever can open the socket can call any endpoint on it. That is the whole point of the probe, because the API is not read-only — the same interface that lists containers can also create them, bind host paths into them and start processes. Access to the daemon is effectively root on the host it runs on. Microsoft-style credential files leak secrets; this leaks the machine.
So a 200 to /containers/json is both the reconnaissance result and the proof of access. Why the request lands on ports 80 and 443 at all is simple: scanners replay the same paths against every HTTP service they find, and a minority of hosts genuinely serve them there — through a reverse-proxy location that forwards to docker.sock, a container-management dashboard, or a socket-proxy container published on a public port. See the Docker Engine API reference for what each endpoint returns.
Who sends it
Undirected, internet-wide traffic. The senders are mass scanners sweeping address ranges for management interfaces, cryptomining botnets — exposed Docker daemons have been a standing target for mining deployment for years — and research crawlers cataloguing exposures. Nothing about these requests means your organisation was chosen.
The user agent is weak evidence: Go-http-client, python-requests and curl are common, and so is a copied Chrome string. The shape is clearer. A handful of requests from one address inside a second or two, no Referer, no CSS, JavaScript or image requests, and the same run usually also asking for other unauthenticated management endpoints — /actuator/health, /.env, metadata and API paths. Your log will often show the same addresses on the Spring Boot Actuator and /.env pages.
Is your server exposed?
The web log answers only half the question; the daemon’s listener answers the other half.
# Which Docker API paths were requested, and what they got
awk '$7 ~ /(containers|images)\/json/ {print $9, $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
# Anything that was NOT 404/403 — this is the list that matters
awk '$7 ~ /(containers|images)\/json/ && $9 !~ /^(404|403)$/' /var/log/nginx/access.log
# Ask your own server, from your own server, and read the status code
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/containers/json
curl -si http://127.0.0.1/v1.40/containers/json | head -12 # look at the headers
# Does the daemon listen on TCP anywhere?
ss -ltnp | grep -E ':(2375|2376)\b'
grep -i hosts /etc/docker/daemon.json 2>/dev/null
systemctl cat docker.service 2>/dev/null | grep -- ' -H '
Repeat the curl checks with the Host header of every vhost you serve — a rule added to the main site and forgotten on a staging or admin vhost is the usual way an internal endpoint stays reachable. Give rotated logs the same awk pass, since an exposure may have been closed after it was used.
How to block it
The durable fix is that the daemon should not be reachable from the network at all: leave it on the Unix socket, use SSH or TLS with client certificates if you must reach it remotely, and keep 2375 and 2376 closed at the firewall and in cloud security groups. Never proxy_pass to docker.sock from a location that does not authenticate first; if a dashboard needs the API, restrict it to the endpoints it uses rather than passing everything through.
Then answer the probe cheaply at the edge:
# Docker Engine API probes — this server proxies no daemon
location ~ ^/(?:v\d+\.\d+/)?(?:containers|images)/json$ {
access_log off;
return 404;
}
The pattern is anchored on the exact endpoint names so it cannot swallow a real /images/ directory of static assets, and it covers both the bare and version-prefixed forms. Returning 404 rather than 403 tells the scanner nothing. Alert on any non-404 response to these paths instead of on the requests themselves — the requests will not stop, and only the answers matter.
Probes that arrive with it
- GET /actuator Requests in Your Logs: Spring Boot Actuator Probing An automated scan for Spring Boot Actuator management endpoints, which expose configuration, environment properties and full JVM heap dumps whenever they are reachable without authentication.
- GET /mcp and /sse Requests in Your Logs: Scanners Hunting MCP Servers An automated scan for Model Context Protocol endpoints — the HTTP paths an MCP server exposes for tool calls and its Server-Sent Events stream — looking for AI integrations that answer without authentication.
- GET /.env Requests in Your Logs: Scanners Hunting for App Secrets An automated scan for application .env files, which hold database passwords, API keys and other secrets in plain text whenever the web server hands one back.
- GET /.aws/credentials Requests in Your Logs: Scanners Hunting AWS Keys An automated scan for the AWS shared credentials and config files, which hold long-lived access keys and profile settings in plain text whenever a web server hands one back.
Detect it
- Sigma rule LTR-0010: Detect Path Traversal & Sensitive-File Recon in Cloudflare Logs →
- Sigma rule LTR-0019: Detect Web Attack Tools (sqlmap, Nikto, Nmap) in AWS ALB / ELB Access Logs →
- Sigma rule LTR-0018: Detect Web Attack Recon and SQL Injection in Zeek HTTP Logs →
- Sigma rule LTR-0021: Detect Managed Rule Group Exploit Blocks in AWS WAF Logs (SQLi, Log4Shell, XSS) →
Frequently Asked Questions
- Everything returned 404 on our web server. Is there anything left to check?
- Yes, but not in the web log. A 404 from nginx only proves that port 80/443 does not proxy the Docker API; it says nothing about the daemon itself. Check whether dockerd listens on a TCP port on any interface, and whether a firewall or security group leaves 2375 or 2376 reachable from outside, because the same scanners hit those ports directly and that traffic never reaches nginx.
- How do I recognise a real Docker API answer in the log or in a curl test?
- A daemon answers this endpoint with HTTP 200, a JSON array as the body and Content-Type application/json, and its responses carry Docker's own headers such as Api-Version and a Server header naming the engine version. An application 404 page or a JSON API of your own will not carry those headers, so compare headers rather than guessing from the status code alone.
- We proxy a container dashboard through nginx. Could these probes reach the daemon through it?
- Only if a location in that vhost forwards unauthenticated requests toward the socket or toward a socket-proxy container. Review every proxy_pass that points at docker.sock or at a container-management backend, confirm authentication is enforced by nginx or the backend before the request is forwarded, and test the exact probe paths against that vhost's Host header rather than against the default server.
- One of these requests returned 200. What is the response?
- Treat the host as compromised rather than just exposed. Anyone who reached the API could create containers, mount host paths and run processes as root, so list containers and images for ones you did not deploy, look for privileged containers or host bind mounts, rotate every credential the host or its containers held, and rebuild rather than clean up in place.
See what else is probing your server
Drop an nginx or Apache access log into the free browser analyzer: secret-file sweeps, exploit probes, scanners and credential stuffing are grouped by source with the evidence lines, and every known probe links back here. Nothing is uploaded.