From b99e0c50275c8d9d0432cb09a809aedce01887d3 Mon Sep 17 00:00:00 2001 From: Jingze <52855280+Jing-ze@users.noreply.github.com> Date: Fri, 22 May 2026 17:01:08 +0800 Subject: [PATCH] fix(ai-proxy): re-anchor X-HI-ORIGINAL-AUTH on first hop to fix cascaded-gateway 401 (#3864) Signed-off-by: jingze Co-authored-by: Claude Opus 4.7 --- plugins/wasm-go/extensions/ai-proxy/main.go | 34 +++++++++++++++++-- .../wasm-go/extensions/ai-proxy/util/http.go | 21 ++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/plugins/wasm-go/extensions/ai-proxy/main.go b/plugins/wasm-go/extensions/ai-proxy/main.go index fec35f69..e6190948 100644 --- a/plugins/wasm-go/extensions/ai-proxy/main.go +++ b/plugins/wasm-go/extensions/ai-proxy/main.go @@ -158,10 +158,38 @@ func initContext(ctx wrapper.HttpContext) { for _, originHeader := range headerToOriginalHeaderMapping { _ = proxywasm.RemoveHttpRequestHeader(originHeader) } - originalAuth, _ := proxywasm.GetHttpRequestHeader(util.HeaderOriginalAuth) - if originalAuth == "" { + + // Distinguish "first hop into this gateway" from "internal_redirect re-entry". + // + // Signal: x-higress-fallback-from. It is set by Envoy custom_response's + // RedirectPolicy on every internal_redirect within this gateway, and it + // survives mutateRequestHeaders on the redirected stream (it is NOT in + // Envoy's hardcoded strip list). + // + // Absence => first hop. Distrust any incoming X-HI-ORIGINAL-AUTH — it may + // have been set by a client or by an upstream cascaded gateway + // running its own ai-proxy. Re-anchor the saved value from the + // request's current Authorization, which is what this gateway + // should treat as the "original" credential for later + // internal_redirect hops. + // Presence => internal_redirect re-entry within this gateway. Leave + // X-HI-ORIGINAL-AUTH alone — it preserves the value this gateway's + // ai-proxy wrote on the previous pass, which key-auth needs for + // re-authentication after Authorization has been replaced with + // the upstream apiToken. + // + // SAFETY DEPENDENCY: this signal is reliable only when external callers + // cannot supply x-higress-fallback-from. For cascaded deployments where an + // upstream gateway may itself be in an internal_redirect chain when forwarding + // to this gateway, list x-higress-fallback-from (and x-hi-original-auth) in + // the HCM internal_only_headers as defense-in-depth. + fallbackFrom, _ := proxywasm.GetHttpRequestHeader(util.HeaderHigressFallbackFrom) + if fallbackFrom == "" { + _ = proxywasm.RemoveHttpRequestHeader(util.HeaderOriginalAuth) value, _ := proxywasm.GetHttpRequestHeader(util.HeaderAuthorization) - ctx.SetContext(ctxOriginalAuth, value) + if value != "" { + ctx.SetContext(ctxOriginalAuth, value) + } } } diff --git a/plugins/wasm-go/extensions/ai-proxy/util/http.go b/plugins/wasm-go/extensions/ai-proxy/util/http.go index 38825301..1bec3800 100644 --- a/plugins/wasm-go/extensions/ai-proxy/util/http.go +++ b/plugins/wasm-go/extensions/ai-proxy/util/http.go @@ -19,6 +19,27 @@ const ( HeaderOriginalHost = "X-ENVOY-ORIGINAL-HOST" HeaderOriginalAuth = "X-HI-ORIGINAL-AUTH" + // HeaderHigressFallbackFrom is set by Envoy custom_response's RedirectPolicy + // on internal_redirect (request_headers_to_add) and survives the redirect's + // mutateRequestHeaders pass (it is NOT in Envoy's hardcoded strip list). So + // its presence at the wasm boundary is a usable signal that the current + // filter-chain pass is an internal_redirect re-entry within this gateway. + // + // SAFETY DEPENDENCY: this header is NOT spoofing-proof unless the listener + // lists it in internal_only_headers. An upstream gateway that is itself in + // the middle of an internal_redirect chain may forward this header through + // to this gateway, causing this gateway's first hop to be misclassified as + // a re-entry. Operators relying on cascaded ai-proxy gateways should add + // `x-higress-fallback-from` and `x-hi-original-auth` to the listener's + // internal_only_headers list as defense-in-depth. + // + // Note: x-envoy-original-url (which Envoy sets on every internal_redirect + // in router.cc) is NOT usable here, because Envoy's recreateStream re-runs + // mutateRequestHeaders on the redirected stream and strips x-envoy-original-url + // from the hardcoded "headers to be stripped from edge AND intermediate-hop + // external requests" list — so wasm filters never see it on a redirect. + HeaderHigressFallbackFrom = "x-higress-fallback-from" + MimeTypeTextPlain = "text/plain" MimeTypeApplicationJson = "application/json" )