2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00
2026-09-28 14:55:53 -04:00

SideDoor

Finds services in Docker Compose files that are reachable without going through your reverse proxy.

What it does and why

Policy enforced at a reverse proxy (geo-blocking, authentication, rate limits, TLS) applies only to traffic that passes through the proxy. A container that publishes a host port sidesteps all of it. Nothing in the proxy's own configuration can tell you this happened, because the proxy does not know the door exists. You can read the whole proxy config and never learn that a sibling container opened port 2222 on the host.

So SideDoor looks at published host ports, not at the proxy config. You pass it parsed compose services, and it reports every binding that opens a path around the proxy:

Binding Result
A sensitive port published on the host (SSH, telnet, PostgreSQL, MySQL, Redis, MongoDB, Elasticsearch, AMQP, memcached, the Docker API) Fail, critical
Any other published host port Fail, high
A port bound to loopback (127.0.0.1, localhost, ::1) Not applicable, still printed: not reachable from the internet, but any process on the host reaches it without the proxy
The proxy itself publishing 80 or 443 Ignored. That is the edge doing its job.
No published ports Counted as internal, the safe shape

A traefik.enable=true label marks a service as proxied, but it does not excuse a published port next to it. Being routable through the proxy says nothing about the door beside it. The proxy service names (traefik, proxy, caddy, nginx-proxy) and edge ports (80, 443) can be changed with edgeServices and edgePorts.

Findings are sorted worst first, so a critical never hides under a page of info lines. An empty scan is reported as not assessed, never as clean. When there is nothing to report, the output says "No unproxied public bindings found" instead of leaving a blank, because a gap of this kind is invisible and a clean result has to be stated.

The library is pure: no Docker, no network and no filesystem. You parse the compose files; it judges them. Reports use the @extant2000/evidence-record format, and every finding cites the compose file, the line when your parser tracked one, and the exact port mapping.

Usage

import { scan, toReport, formatScan } from '@extant2000/side-door'

const result = scan([
  { name: 'traefik', file: 'compose.yml', line: 3, ports: ['80:80', '443:443'] },
  { name: 'app', file: 'compose.yml', line: 12, labels: ['traefik.enable=true'] },
  { name: 'db', file: 'compose.yml', line: 20, ports: ['5432:5432'] },
  { name: 'cache', file: 'compose.yml', line: 28, ports: ['127.0.0.1:6379:6379'] },
  { name: 'worker', file: 'compose.yml', line: 34 },
])

const report = toReport(result)   // evidence-record report
console.log(formatScan(result))

The finding lines from that output:

✗ CRITICAL db publishes 5432:5432   [NOT MET]
◦ info     cache publishes 127.0.0.1:6379:6379   [N/A]

parseBinding() is exported too. It handles 80, 8080:80, 127.0.0.1:8080:80 and 8080:80/tcp, and returns null for anything it cannot parse. A bare 80 still publishes a host port.

Install

npm install @extant2000/side-door

License

MIT. See LICENSE.

S
Description
Finds services that can be reached without going through your reverse proxy.
Readme MIT
49 KiB
Languages
TypeScript 100%