Harden the read path, forwarded headers and security docs
Caddy Manager CI build / docker (push) Failing after 1m2s
Caddy Manager CI build / docker (push) Failing after 1m2s
Found while investigating an unrelated Gitea compromise: CaddyManager itself was not involved, but reviewing it turned up three things worth closing. Reading a configuration was the only file operation that did not validate the name. Saving, renaming and deleting all reject `..`, `/` and `\`, so the read path was the one way to leave the configuration directory and pull in any `*.caddy` file on the host. The HTTP API happened to be covered, because GET checks the name against the directory listing first, but the UI calls the service directly and nothing stopped it. Forwarded headers were trusted from any peer. That is correct only while the container port is unreachable except through the proxy; the moment it is published, a caller dictates the scheme, host and client address the app believes in. Loopback and private space cover a proxy on a Docker network or on the host, which is the documented deployment, and ignore everyone else. The README never said that the `X-Api-Key` check guards `/api/*` and nothing else, so the UI - which rewrites Caddyfiles and holds the Docker socket - reads as protected when it is not. It now says so, and warns about the specific shape that bit us: a second hostname added for machine callers whose only extra directive is a `tls` line, which serves the unauthenticated UI to anyone who can resolve it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,38 @@ yourself (the UI warns about this too).
|
||||
> Note: the app redirects HTTP to HTTPS, so a direct `curl http://...` against the container port
|
||||
> gets a `307`. Add `-L`, call it over HTTPS, or go through your reverse proxy.
|
||||
|
||||
### Security model
|
||||
|
||||
Read this before exposing the container port anywhere.
|
||||
|
||||
**The web UI has no authentication of its own.** The `X-Api-Key` check applies to `/api/*` only.
|
||||
Everything else — the whole Blazor UI, which can rewrite any `*.caddy` file, replace the global
|
||||
Caddyfile, and reload or restart Caddy — is served to whoever can open the port. The container also
|
||||
mounts the Docker socket in order to reload Caddy, so control of the UI is control of the Docker
|
||||
daemon on that host.
|
||||
|
||||
So: **put an authenticating reverse proxy in front of this app, and do not publish its port past
|
||||
that proxy.** Any of the usual options works — the maintainer's own deployment uses
|
||||
[Authentik](https://goauthentik.io/) forward auth:
|
||||
|
||||
```caddy
|
||||
example.com {
|
||||
route {
|
||||
import authentik_forwardauth
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two things worth checking in your own setup:
|
||||
|
||||
- If you expose a second hostname for machine callers (so scripts can use `X-Api-Key` without going
|
||||
through SSO), scope it to `/api/*` and refuse the rest, or that hostname serves the unauthenticated
|
||||
UI as well. A hostname whose only extra directive is a `tls` line is **not** access control.
|
||||
- `X-Forwarded-*` headers are honoured only from loopback and private address space (RFC 1918 /
|
||||
RFC 4193). A proxy on a Docker network or on the host satisfies this; a proxy reaching the app from
|
||||
a public address does not, and will see the app fall back to the real peer address and `http`.
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user