# 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 ```ts 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 The package is published to the GitLab package registry, so point the `@extant2000` scope there first: ``` npm config set @extant2000:registry https://gitlab.com/api/v4/packages/npm/ npm install @extant2000/side-door ``` ## License MIT. See [LICENSE](LICENSE).