Harden the read path, forwarded headers and security docs
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:
2026-09-12 17:05:52 +07:00
co-authored by Claude Opus 5
parent 8f0019592e
commit c484014752
4 changed files with 124 additions and 2 deletions
+32
View File
@@ -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>