Files
Paul HittandClaude Opus 5.5 d0cb30a829 Publish to the GitLab package registry on version tags
The README's install line failed: the package was never published, and
nothing told npm where the @extant2000 scope lives. A tag like v1.2.3 now
publishes to this project's registry with CI_JOB_TOKEN, and the README
sets the scope's registry before installing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:42:35 -04:00

85 lines
3.4 KiB
Markdown

# 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).