mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
docs(sowerd): keep the site-route authority rule in one place
Second half of the review: the rationale for `authority` had been written into
every comment that touched it, which is three copies to keep in sync. Keep the
argument in docs/security.md and leave the config example, the struct comment
and the constants stating only what the value does.
- docs/security.md: record "an upstream trusting X-Forwarded-Host" under
Rejected Directions, where the document already collects declined designs,
instead of inside the new rule section; merge the per-route bullet into the
client-controlled one they overlapped with.
- ARCHITECTURE.md: the sentence added last commit left the fake-site rationale
dangling after the site-route clause; split it back into one claim per
sentence.
- docs/plans/site-routing.md: the plan states the unconditional Host rewrite as
a decision of its time, so it gets a forward note to the authoritative
document rather than a rewrite of the record.
- config/sowerd.go: clearer validation error ("unknown value", naming both
accepted values) and a tighter comment; no behavior change beyond the
message.
This commit is contained in:
+1
-1
@@ -142,7 +142,7 @@ For non-transport fallback traffic, it can route by TLS SNI to per-domain upstre
|
||||
- `sowerd` prefers the user cache directory for ACME state, but falls back to `/var/cache/sower` so systemd services can start without `HOME`/`XDG_CACHE_HOME` or a config file.
|
||||
- `sowerd` fallback site routing is based only on exact TLS SNI matches; wildcard domains are not supported.
|
||||
- `sowerd` site routes use HTTP reverse proxying to support full `http://` and `https://` upstream URLs. The reverse proxy rewrites the outbound Host to the upstream host; a route can set `authority = "client"` to forward the client's authority instead, for upstreams that validate the authority themselves (see [docs/security.md](docs/security.md)).
|
||||
- `sowerd` is the TLS edge for fallback HTTP. Reverse-proxied site routes and HTTP fake-site fallback overwrite `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Real-IP` from the immediate client; client-supplied forwarding headers are discarded. `X-Forwarded-Proto` is `https` because this path is the 443 listener after TLS termination. `X-Forwarded-Host` always carries the original authority; HTTP fake-site fallback and site routes with `authority = "client"` keep that authority in the Host header itself, matching the previous TCP relay for the fallback.
|
||||
- `sowerd` is the TLS edge for fallback HTTP. Reverse-proxied site routes and HTTP fake-site fallback overwrite `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Real-IP` from the immediate client; client-supplied forwarding headers are discarded. `X-Forwarded-Proto` is `https` because this path is the 443 listener after TLS termination. `X-Forwarded-Host` always carries the original authority. The HTTP fake-site fallback has always kept that authority in the Host header itself, matching the previous TCP relay; a site route keeps it too when the route sets `authority = "client"`.
|
||||
- `sowerd` reverse-proxied fallback supports HTTP/1.1 protocol upgrades (including websocket) through the standard library ReverseProxy hijack path.
|
||||
- `sowerd` site routing applies bounded client header, upstream dial, TLS handshake, and upstream response-header timeouts.
|
||||
- `sowerd` advertises only HTTP/1.1 over TLS because fallback site routing and fake-site serving are HTTP/1.1 paths.
|
||||
|
||||
+10
-18
@@ -35,10 +35,8 @@ fake_site = "/var/www" # Fallback fake site directory or address (def
|
||||
# upstream = "http://127.0.0.1:8080"
|
||||
#
|
||||
# authority selects the Host header sent upstream: "upstream" (the default)
|
||||
# sends the route's upstream host, "client" forwards the authority the client
|
||||
# asked for. Use "client" only for an upstream that validates the authority
|
||||
# itself (an exact host allowlist, or Origin against Host), because the
|
||||
# forwarded name is client-controlled.
|
||||
# sends the route's upstream host, "client" forwards the client's authority,
|
||||
# for an upstream that validates that authority itself (see docs/security.md).
|
||||
# authority = "client"
|
||||
#
|
||||
# [site_routes.auth]
|
||||
@@ -77,12 +75,9 @@ type SiteRoute struct {
|
||||
Upstream string `usage:"upstream URL (http:// or https://)"`
|
||||
// Authority selects the Host header presented to the upstream: the empty
|
||||
// value and AuthorityUpstream send the upstream host, AuthorityClient
|
||||
// forwards the authority the client asked for. An upstream that validates
|
||||
// the visited authority itself (an exact host allowlist, or an
|
||||
// Origin-versus-Host comparison) needs the client's name and rejects the
|
||||
// rewritten one, so the choice is explicit and per route. Either way the
|
||||
// forwarded name is client-controlled: only opt in for an upstream that
|
||||
// checks it itself.
|
||||
// forwards the authority the client asked for. Only an upstream that
|
||||
// validates that authority itself wants the client's name; the reasoning is
|
||||
// in docs/security.md.
|
||||
Authority string `usage:"authority presented to the upstream: upstream (default) or client"`
|
||||
Routes map[string]string `usage:"path-prefix to upstream URL overrides (e.g. /ws = http://127.0.0.1:8082)"`
|
||||
// Auth is an optional edge-side gate for visitors of this route. The
|
||||
@@ -187,12 +182,10 @@ func (c *SowerdConfig) Validate() error {
|
||||
|
||||
// Authority values accepted by SiteRoute.Authority.
|
||||
const (
|
||||
// AuthorityUpstream sends the route's upstream host as the Host header. It
|
||||
// is the default, and the behavior of every route that leaves the field
|
||||
// unset.
|
||||
// AuthorityUpstream sends the route's upstream host as the Host header; it
|
||||
// is the default.
|
||||
AuthorityUpstream = "upstream"
|
||||
// AuthorityClient forwards the authority the client asked for, for an
|
||||
// upstream that validates that authority itself.
|
||||
// AuthorityClient forwards the authority the client asked for.
|
||||
AuthorityClient = "client"
|
||||
)
|
||||
|
||||
@@ -201,14 +194,13 @@ const (
|
||||
func (r SiteRoute) PreserveHost() bool { return r.Authority == AuthorityClient }
|
||||
|
||||
// validateSiteRouteAuthority rejects a typo'd authority instead of silently
|
||||
// falling back to the default: the two values differ in whether the upstream
|
||||
// sees a client-controlled name.
|
||||
// falling back to the default, which is the value that hides the client's name.
|
||||
func validateSiteRouteAuthority(i int, authority string) error {
|
||||
switch authority {
|
||||
case "", AuthorityUpstream, AuthorityClient:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("site_routes[%d].authority: %q is not %q, %q, or empty", i, authority, AuthorityUpstream, AuthorityClient)
|
||||
return fmt.Errorf("site_routes[%d].authority: unknown value %q (want %q or %q)", i, authority, AuthorityUpstream, AuthorityClient)
|
||||
}
|
||||
|
||||
func validateSiteRoutes(routes []SiteRoute) error {
|
||||
|
||||
+2
-4
@@ -22,10 +22,8 @@ fake_site = "/var/www" # Fallback fake site directory or address (def
|
||||
# upstream = "http://127.0.0.1:8080"
|
||||
#
|
||||
# authority selects the Host header sent upstream: "upstream" (the default)
|
||||
# sends the route's upstream host, "client" forwards the authority the client
|
||||
# asked for. Use "client" only for an upstream that validates the authority
|
||||
# itself (an exact host allowlist, or Origin against Host), because the
|
||||
# forwarded name is client-controlled.
|
||||
# sends the route's upstream host, "client" forwards the client's authority,
|
||||
# for an upstream that validates that authority itself (see docs/security.md).
|
||||
# authority = "client"
|
||||
#
|
||||
# [site_routes.auth]
|
||||
|
||||
@@ -26,4 +26,4 @@
|
||||
## 已推翻的实现决策
|
||||
|
||||
- ~~reverse proxy 会拒绝 HTTP upgrade 请求~~ — **已推翻**:现行实现不做任何拒绝,`httputil.ReverseProxy` 在 101 时经 hijack 直通,`TestReverseProxyConnWebSocket` 与 `TestReverseProxyConnPathRoutedWebSocket` 均通过,见 `ARCHITECTURE.md`。
|
||||
- reverse proxy 会把 outbound `Host` 显式改写为 upstream Host,路径和查询参数保留;并设置客户端 header、upstream dial、TLS handshake、upstream response header 超时。
|
||||
- reverse proxy 会把 outbound `Host` 显式改写为 upstream Host,路径和查询参数保留;并设置客户端 header、upstream dial、TLS handshake、upstream response header 超时。(更新 v1.7.1:route 可设 `authority = "client"` 保留客户端 authority,见 [docs/security.md](../security.md);本条描述默认行为。)
|
||||
|
||||
+24
-23
@@ -154,29 +154,25 @@ Rules:
|
||||
authority or the upstream's own host.**
|
||||
|
||||
- By default the reverse proxy rewrites the outbound `Host` to the route's
|
||||
upstream host and reports the client's authority in `X-Forwarded-Host`.
|
||||
An upstream that virtual-hosts on its own address keeps working, and a
|
||||
misconfigured route cannot make an upstream believe it serves a name it
|
||||
does not own.
|
||||
- `authority = "client"` forwards the client's `Host` verbatim instead. This
|
||||
is what an upstream that validates the visited authority itself needs — an
|
||||
exact host allowlist, or an `Origin` versus `Host` comparison — because it
|
||||
rejects the rewritten host: pi-web answers `403` to every browser-originated
|
||||
`/api` request, so the page loads and its WebSocket never connects.
|
||||
- The forwarded authority is client-controlled, so preserving it does not make
|
||||
the upstream safe by itself: the upstream must still validate the name, and
|
||||
its allowlist has to include the public domain (pi-web's
|
||||
`PI_WEB_ALLOWED_HOSTS`). The route's visitor gate still runs, and inbound
|
||||
`X-Forwarded-*` headers from the client are still discarded, so one request
|
||||
cannot tell the upstream two different stories.
|
||||
- Keep it per route. An upstream that does not check the authority accepts any
|
||||
name, and `authority = "client"` would then let a prober choose what that
|
||||
upstream records in its logs, emits in redirects, and sets as a cookie
|
||||
domain.
|
||||
- Rejected alternative: teaching the upstream to trust `X-Forwarded-Host`.
|
||||
Those headers are client-forgeable wherever sowerd is not the immediate
|
||||
proxy, so the adaptation belongs to the edge, which already owns the
|
||||
connection and strips what the client supplied.
|
||||
upstream host and reports the client's authority in `X-Forwarded-Host`. An
|
||||
upstream that virtual-hosts on its own address keeps working, and a
|
||||
misconfigured route cannot make an upstream believe it serves a name it does
|
||||
not own.
|
||||
- `authority = "client"` forwards the client's `Host` verbatim instead. This is
|
||||
what an upstream that validates the visited authority itself needs — an exact
|
||||
host allowlist, or an `Origin` versus `Host` comparison — because it rejects
|
||||
the rewritten host: pi-web answers `403` to every browser-originated `/api`
|
||||
request, so the page loads and its realtime stream never connects. The
|
||||
forwarded authority is client-controlled, so preserving it protects nothing
|
||||
by itself: the upstream still has to check the name, and its allowlist has to
|
||||
include the public domain (pi-web's `PI_WEB_ALLOWED_HOSTS`). The route's
|
||||
visitor gate still runs, and inbound `X-Forwarded-*` headers from the client
|
||||
are still discarded, so one request cannot tell the upstream two different
|
||||
stories.
|
||||
- Opt in per route, because an upstream that does not check the authority
|
||||
accepts any name: with `"client"`, a prober chooses what that upstream records
|
||||
in its logs, emits in redirects, and sets as a cookie domain. That is why the
|
||||
default stays the upstream host.
|
||||
|
||||
## Unknown-SNI Certificate Fallback
|
||||
|
||||
@@ -259,6 +255,11 @@ Decisions recorded here so they are not re-litigated without new evidence:
|
||||
unknown SNI (probes and failed transport auth included) and must keep
|
||||
answering like an ordinary public website. An operator's real site that needs
|
||||
a gate belongs in `site_routes`.
|
||||
- **An upstream trusting `X-Forwarded-Host` instead of having its `Host`
|
||||
forwarded** — the adaptation belongs to the edge: forwarding headers are
|
||||
client-forgeable wherever sowerd is not the immediate proxy, while the edge
|
||||
already owns the connection and discards what the client supplied. An
|
||||
upstream that needs its own name gets it through `authority = "client"`.
|
||||
- **Rejecting known-weak passwords in `SowerdConfig.Validate`** — declined: a
|
||||
blocklist of placeholder strings is not a strength policy (it catches
|
||||
`change_me` and nothing else), and it would stop existing deployments from
|
||||
|
||||
Reference in New Issue
Block a user