diff --git a/Makefile b/Makefile index 47e4f59..4c15786 100644 --- a/Makefile +++ b/Makefile @@ -95,8 +95,16 @@ docker/shell: ## Open a shell inside the running backend container $(COMPOSE) exec backend /bin/bash .PHONY: docker/run +# Published on loopback, like compose, `docker/aio` and both smoke scripts. The +# server binds 127.0.0.1 by default precisely because it has no authentication, +# but the image's ENTRYPOINT pins --host 0.0.0.0 so a published port reaches it +# at all — which means the port mapping is the only thing left deciding who can +# talk to it. `-p $(COLLAPSE_PORT):8000` binds every interface, so on a laptop +# on a cafe network this target alone put an unauthenticated compression +# service on the LAN. Publish wider by hand, on a network you have thought +# about. docker/run: docker/build ## Run a throwaway container — ARGS="--max-upload-mb 50" - docker run --rm -p $(COLLAPSE_PORT):8000 $(IMAGE) $(ARGS) + docker run --rm -p 127.0.0.1:$(COLLAPSE_PORT):8000 $(IMAGE) $(ARGS) .PHONY: docker/smoke docker/smoke: ## Build, start, drive a real compression through the published port, stop diff --git a/docs/server.md b/docs/server.md index 3e1a93d..89f9a52 100644 --- a/docs/server.md +++ b/docs/server.md @@ -604,6 +604,45 @@ Everything below follows from that: The full picture, including what the server does defend against when it unpacks a tar someone sent it, is in [threat_model.md](threat_model.md#the-api-server). +### Putting it behind something that authenticates + +"Put it behind a proxy" is the standing advice above, and it is worth spelling +out, because **the CLI already works with the most ordinary form of it** and +nothing in the codebase said so. + +`ureq` sends HTTP Basic credentials from a URL's userinfo, so a reverse proxy +that asks for a password needs no client change: + +```bash +collapse compress notes.txt --server http://user:secret@proxy.internal:8080 +``` + +Verified against a stub that requires authentication: the request arrives with +`Authorization: Basic dXNlcjpzZWNyZXQ=`, and a rejection is reported legibly +rather than as a transport failure — + +``` +error: the server rejected the request (HTTP 401): authentication required +``` + +— because the CLI prefers the server's JSON `detail` field when it has one. + +**What this costs, stated rather than left to be discovered.** The credential is +in the command, so it is in shell history and in the process list while the +command runs. If the same URL is saved as a server in the desktop app it is +written to the webview's `localStorage` in clear text (`apps/desktop/src/ +sources.js`), which is a file on disk with no protection beyond the user +account. Prefer a credential minted for this purpose over a password that +unlocks anything else. + +**What the client cannot do.** There is no way to send a bearer token, an API +key header, or a client certificate. Basic through a proxy is the whole of it +today, which is enough for "keep strangers off it" and is not enough for +per-client identity or revocation. + +Terminating TLS at that proxy costs nothing extra on the client: `ureq` is built +with rustls, so `--server https://…` works the moment something answers on it. + ## Known limitations Worth knowing before deploying this unattended: diff --git a/docs/threat_model.md b/docs/threat_model.md index d79d56e..0188994 100644 --- a/docs/threat_model.md +++ b/docs/threat_model.md @@ -358,7 +358,10 @@ Stated plainly, because deploying it assumes these: - **No authentication and no rate limiting.** Anyone who can reach the port can submit jobs, and jobs consume CPU, memory and disk. Bind it to localhost (the default, and what the container image publishes) or put it behind something - that authenticates. + that authenticates. That second half is written out in + [server.md](server.md#putting-it-behind-something-that-authenticates): + the CLI already carries Basic credentials through a proxy, and what that + costs is stated there too. - **The web frontend proxies the API, so its port exposes the API too.** nginx forwards `/compress` and `/jobs` to the backend, which is what keeps the browser same-origin; the consequence is that publishing the web port to a