mirror of
https://github.com/wweir/sower.git
synced 2026-10-20 08:16:35 +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` 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` 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` 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` 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` 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.
|
- `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"
|
# upstream = "http://127.0.0.1:8080"
|
||||||
#
|
#
|
||||||
# authority selects the Host header sent upstream: "upstream" (the default)
|
# authority selects the Host header sent upstream: "upstream" (the default)
|
||||||
# sends the route's upstream host, "client" forwards the authority the client
|
# sends the route's upstream host, "client" forwards the client's authority,
|
||||||
# asked for. Use "client" only for an upstream that validates the authority
|
# for an upstream that validates that authority itself (see docs/security.md).
|
||||||
# itself (an exact host allowlist, or Origin against Host), because the
|
|
||||||
# forwarded name is client-controlled.
|
|
||||||
# authority = "client"
|
# authority = "client"
|
||||||
#
|
#
|
||||||
# [site_routes.auth]
|
# [site_routes.auth]
|
||||||
@@ -77,12 +75,9 @@ type SiteRoute struct {
|
|||||||
Upstream string `usage:"upstream URL (http:// or https://)"`
|
Upstream string `usage:"upstream URL (http:// or https://)"`
|
||||||
// Authority selects the Host header presented to the upstream: the empty
|
// Authority selects the Host header presented to the upstream: the empty
|
||||||
// value and AuthorityUpstream send the upstream host, AuthorityClient
|
// value and AuthorityUpstream send the upstream host, AuthorityClient
|
||||||
// forwards the authority the client asked for. An upstream that validates
|
// forwards the authority the client asked for. Only an upstream that
|
||||||
// the visited authority itself (an exact host allowlist, or an
|
// validates that authority itself wants the client's name; the reasoning is
|
||||||
// Origin-versus-Host comparison) needs the client's name and rejects the
|
// in docs/security.md.
|
||||||
// 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.
|
|
||||||
Authority string `usage:"authority presented to the upstream: upstream (default) or client"`
|
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)"`
|
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
|
// 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.
|
// Authority values accepted by SiteRoute.Authority.
|
||||||
const (
|
const (
|
||||||
// AuthorityUpstream sends the route's upstream host as the Host header. It
|
// 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
|
// is the default.
|
||||||
// unset.
|
|
||||||
AuthorityUpstream = "upstream"
|
AuthorityUpstream = "upstream"
|
||||||
// AuthorityClient forwards the authority the client asked for, for an
|
// AuthorityClient forwards the authority the client asked for.
|
||||||
// upstream that validates that authority itself.
|
|
||||||
AuthorityClient = "client"
|
AuthorityClient = "client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -201,14 +194,13 @@ const (
|
|||||||
func (r SiteRoute) PreserveHost() bool { return r.Authority == AuthorityClient }
|
func (r SiteRoute) PreserveHost() bool { return r.Authority == AuthorityClient }
|
||||||
|
|
||||||
// validateSiteRouteAuthority rejects a typo'd authority instead of silently
|
// validateSiteRouteAuthority rejects a typo'd authority instead of silently
|
||||||
// falling back to the default: the two values differ in whether the upstream
|
// falling back to the default, which is the value that hides the client's name.
|
||||||
// sees a client-controlled name.
|
|
||||||
func validateSiteRouteAuthority(i int, authority string) error {
|
func validateSiteRouteAuthority(i int, authority string) error {
|
||||||
switch authority {
|
switch authority {
|
||||||
case "", AuthorityUpstream, AuthorityClient:
|
case "", AuthorityUpstream, AuthorityClient:
|
||||||
return nil
|
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 {
|
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"
|
# upstream = "http://127.0.0.1:8080"
|
||||||
#
|
#
|
||||||
# authority selects the Host header sent upstream: "upstream" (the default)
|
# authority selects the Host header sent upstream: "upstream" (the default)
|
||||||
# sends the route's upstream host, "client" forwards the authority the client
|
# sends the route's upstream host, "client" forwards the client's authority,
|
||||||
# asked for. Use "client" only for an upstream that validates the authority
|
# for an upstream that validates that authority itself (see docs/security.md).
|
||||||
# itself (an exact host allowlist, or Origin against Host), because the
|
|
||||||
# forwarded name is client-controlled.
|
|
||||||
# authority = "client"
|
# authority = "client"
|
||||||
#
|
#
|
||||||
# [site_routes.auth]
|
# [site_routes.auth]
|
||||||
|
|||||||
@@ -26,4 +26,4 @@
|
|||||||
## 已推翻的实现决策
|
## 已推翻的实现决策
|
||||||
|
|
||||||
- ~~reverse proxy 会拒绝 HTTP upgrade 请求~~ — **已推翻**:现行实现不做任何拒绝,`httputil.ReverseProxy` 在 101 时经 hijack 直通,`TestReverseProxyConnWebSocket` 与 `TestReverseProxyConnPathRoutedWebSocket` 均通过,见 `ARCHITECTURE.md`。
|
- ~~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.**
|
authority or the upstream's own host.**
|
||||||
|
|
||||||
- By default the reverse proxy rewrites the outbound `Host` to the route's
|
- 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`.
|
upstream host and reports the client's authority in `X-Forwarded-Host`. An
|
||||||
An upstream that virtual-hosts on its own address keeps working, and a
|
upstream that virtual-hosts on its own address keeps working, and a
|
||||||
misconfigured route cannot make an upstream believe it serves a name it
|
misconfigured route cannot make an upstream believe it serves a name it does
|
||||||
does not own.
|
not own.
|
||||||
- `authority = "client"` forwards the client's `Host` verbatim instead. This
|
- `authority = "client"` forwards the client's `Host` verbatim instead. This is
|
||||||
is what an upstream that validates the visited authority itself needs — an
|
what an upstream that validates the visited authority itself needs — an exact
|
||||||
exact host allowlist, or an `Origin` versus `Host` comparison — because it
|
host allowlist, or an `Origin` versus `Host` comparison — because it rejects
|
||||||
rejects the rewritten host: pi-web answers `403` to every browser-originated
|
the rewritten host: pi-web answers `403` to every browser-originated `/api`
|
||||||
`/api` request, so the page loads and its WebSocket never connects.
|
request, so the page loads and its realtime stream never connects. The
|
||||||
- The forwarded authority is client-controlled, so preserving it does not make
|
forwarded authority is client-controlled, so preserving it protects nothing
|
||||||
the upstream safe by itself: the upstream must still validate the name, and
|
by itself: the upstream still has to check the name, and its allowlist has to
|
||||||
its allowlist has to include the public domain (pi-web's
|
include the public domain (pi-web's `PI_WEB_ALLOWED_HOSTS`). The route's
|
||||||
`PI_WEB_ALLOWED_HOSTS`). The route's visitor gate still runs, and inbound
|
visitor gate still runs, and inbound `X-Forwarded-*` headers from the client
|
||||||
`X-Forwarded-*` headers from the client are still discarded, so one request
|
are still discarded, so one request cannot tell the upstream two different
|
||||||
cannot tell the upstream two different stories.
|
stories.
|
||||||
- Keep it per route. An upstream that does not check the authority accepts any
|
- Opt in per route, because an upstream that does not check the authority
|
||||||
name, and `authority = "client"` would then let a prober choose what that
|
accepts any name: with `"client"`, a prober chooses what that upstream records
|
||||||
upstream records in its logs, emits in redirects, and sets as a cookie
|
in its logs, emits in redirects, and sets as a cookie domain. That is why the
|
||||||
domain.
|
default stays the upstream host.
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Unknown-SNI Certificate Fallback
|
## 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
|
unknown SNI (probes and failed transport auth included) and must keep
|
||||||
answering like an ordinary public website. An operator's real site that needs
|
answering like an ordinary public website. An operator's real site that needs
|
||||||
a gate belongs in `site_routes`.
|
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
|
- **Rejecting known-weak passwords in `SowerdConfig.Validate`** — declined: a
|
||||||
blocklist of placeholder strings is not a strength policy (it catches
|
blocklist of placeholder strings is not a strength policy (it catches
|
||||||
`change_me` and nothing else), and it would stop existing deployments from
|
`change_me` and nothing else), and it would stop existing deployments from
|
||||||
|
|||||||
Reference in New Issue
Block a user