From b417b5bedc97e37f788db22f6c78db2104e10b73 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 05:47:17 +0000 Subject: [PATCH] Docs: allowed_hosts never lists the name a proxy rewrites Host to The comment on server.allowed_hosts (config.rs, forge.toml.example) advised adding the name a reverse proxy rewrites Host to. With such a name every request names an allowed host, and the trusted-header realms' DNS-rebinding guard is off. The comment now says the proxy must forward the client's own Host and that a rewritten name must never be listed; the deployment guide's trusted-header section says the same. Fixes #583 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012e6DJXRhLYxFjzrWiwKFKc --- crates/server/src/config.rs | 18 +++++++++++------- docs/changes/2.2.0/583.docs.md | 6 ++++++ docs/deployment.md | 6 +++++- forge.toml.example | 10 +++++++--- 4 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 docs/changes/2.2.0/583.docs.md diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs index c2c598b0..4df17a6c 100644 --- a/crates/server/src/config.rs +++ b/crates/server/src/config.rs @@ -137,13 +137,17 @@ pub struct ServerConfig { /// `true` only for local development / compose stacks. #[serde(default)] pub allow_insecure_defaults: bool, - /// Extra `Host` names (an optional `:port` is ignored) under which the - /// server is legitimately reached, besides the host of - /// [`Self::public_base_url`]. Only consulted for the trusted-header - /// realms: a request whose `Host` matches neither is not named by the - /// headers of the proxy in front (a DNS-rebinding page in an internal - /// user's browser presents its own host name). Add the names a reverse - /// proxy rewrites `Host` to, or that internal clients use. Default empty. + /// Extra `Host` names (an optional `:port` is ignored) under which + /// clients reach the server, besides the host of + /// [`Self::public_base_url`], such as a name internal clients use. Only + /// consulted for the trusted-header realms: a request whose `Host` + /// matches neither is not named by the headers of the proxy in front (a + /// DNS-rebinding page in an internal user's browser presents its own host + /// name). The guard needs the client's own `Host`, so the reverse proxy + /// must forward it (nginx: `proxy_set_header Host $host`). Never add a + /// name the proxy rewrites `Host` to (`forge` for `proxy_pass + /// http://forge:8421`): every request then names an allowed host, and + /// the guard is off. Default empty. #[serde(default)] pub allowed_hosts: Vec, /// Seconds a client may take to send a request's headers before the diff --git a/docs/changes/2.2.0/583.docs.md b/docs/changes/2.2.0/583.docs.md new file mode 100644 index 00000000..6247f343 --- /dev/null +++ b/docs/changes/2.2.0/583.docs.md @@ -0,0 +1,6 @@ +The comment on `server.allowed_hosts` in `forge.toml.example` advised adding +the name a reverse proxy rewrites `Host` to. That turns off the +trusted-header realms' DNS-rebinding guard, since every request then names an +allowed host. The comment and the deployment guide now say that the proxy +must forward the client's own `Host`, and that such a name must never be +listed. diff --git a/docs/deployment.md b/docs/deployment.md index cb4c0a51..b77ba5c8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -981,7 +981,11 @@ What Forge believes, and when: `auth.trusted_proxy_cidrs` and the request's `Host` is one of Forge's own names (the host of `public_base_url`, or one in `server.allowed_hosts`: a DNS-rebinding page reaches the proxy under its own name). From any other - request they are ignored. + request they are ignored. This guard holds only while the proxy forwards + the client's own `Host`: never list the name a proxy rewrites `Host` to + (`forge` for an nginx `proxy_pass http://forge:8421` without + `proxy_set_header Host $host`) in `server.allowed_hosts`, since every + request would then name an allowed host. - They name the caller only when the request has **no credential of its own**: an API token, a password or a signed-in session is that credential's, whatever the headers say, and one that is refused is a 401, diff --git a/forge.toml.example b/forge.toml.example index 9be36188..bb769146 100644 --- a/forge.toml.example +++ b/forge.toml.example @@ -30,9 +30,13 @@ allow_insecure_defaults = true # (default: false) # It also caps the upload limit a repository may be given. Set below 1 GiB, it # lowers the docker defaults (upload limit, docker.max_blob_bytes) to fit. # Host names (port ignored) clients reach this server under besides the host of -# public_base_url, e.g. the name a reverse proxy rewrites Host to. Only gates -# the trusted-header realms (Admin → Authentication): a request naming any -# other host is not named by the proxy's headers (DNS-rebinding guard). +# public_base_url, e.g. a name internal clients use. Only gates the +# trusted-header realms (Admin → Authentication): a request naming any other +# host is not named by the proxy's headers (DNS-rebinding guard). The guard +# needs the client's own Host: the reverse proxy must forward it (nginx: +# proxy_set_header Host $host). Never add a name the proxy rewrites Host to +# ("forge" for proxy_pass http://forge:8421): every request then names an +# allowed host, and the guard is off. allowed_hosts = [] # (default: []) # Connection and request time limits. A client that takes longer to send its # request headers, or whose request body stalls longer, is cut off; a handler