feat(llm): apikey_header auth scheme for Kong AI Gateway key-auth#303
Conversation
…oses #302) Kong AI Gateway's key-auth plugin reads the consumer key from a fixed `apikey` header and ignores provider-native headers, so an agent pointed at a Kong-fronted OPENAI_BASE_URL/ANTHROPIC_BASE_URL 401s every LLM call with "No API key found in request". Extend the existing AuthScheme extension point (added for aws_sigv4 in #202) with auth_scheme: apikey_header — sends the API key in a gateway header IN ADDITION TO the provider-native header (additive, so safe against non-gateway endpoints; Kong's ai-proxy replaces/injects the upstream provider header anyway). Header name defaults to "apikey" (Kong key-auth's default key_names) and is overridable via model.auth_header_name for gateways with custom key names. - llm.ClientConfig / types.ModelRef: new AuthHeaderName field; scheme constants (AuthSchemeAWSSigV4 / AuthSchemeAPIKeyHeader / DefaultAPIKeyHeaderName) replace the scattered "aws_sigv4" literals. - providers/auth_scheme.go: shared setGatewayAPIKeyHeader helper, wired into both anthropic and openai setHeaders (additive, no-op off-path or when APIKey is empty). - runtime/config.go: carry ModelRef.auth_header_name onto the client. - Tests: both providers send both headers; custom header name; no-op for the default scheme and empty key; config carry-over. - Docs: runtime-engine.md, forge-yaml-schema.md, knowledge skill. Platform wiring (agent-builder env → generated forge.yaml) is a follow-up, matching the SigV4 precedent.
initializ-mk
left a comment
There was a problem hiding this comment.
Review: apikey_header auth scheme
The core change is exactly how an extension point should be extended: scheme constants replace the scattered "aws_sigv4" literals, a single shared setGatewayAPIKeyHeader helper with correct no-op semantics is wired into both providers, and the config carry-over mirrors the established aws_region pattern. Tests are exact — both providers assert BOTH headers, the custom-name test also asserts the default header ISN'T set, off-path no-ops are pinned, and the yaml→client mapping is covered. Docs updated in all three places including the knowledge skill. The additive design is correctly reasoned (Kong consumes apikey, ai-proxy manages the upstream header, OpenAI-compatible endpoints ignore unknown headers). CI fully green.
Three minors, all inline:
auth_schemevalues are validated nowhere (verified — no code outside the config mapping touches it): a typo silently reproduces the exact 401 this PR fixes. Recommended ride-along.- Fallbacks can't use the scheme —
ModelFallbackhas no scheme fields andresolveFallbacksbuilds bareClientConfig{APIKey, Model}; worth a documenting sentence or the symmetric fields. - Header collision + upstream key transit —
auth_header_name: Authorizationoverwrites the native Bearer header; Kongkey-authdefaults tohide_credentials: falseso the key transits upstream in an unconventional header.
Verdict
Merge-ready for the happy path; finding 1 strongly recommended as a ride-along — it's a few lines against constants this PR itself introduces, and it converts this PR's target failure mode from "silent 401s" into a config error. Findings 2–3 are a docs sentence each or small follow-ups.
| // AuthScheme == "apikey_header"; carried unconditionally so the client | ||
| // owns the default-name fallback. | ||
| if cfg.Model.AuthHeaderName != "" { | ||
| mc.Client.AuthHeaderName = cfg.Model.AuthHeaderName |
There was a problem hiding this comment.
Minor (recommended ride-along): auth_scheme values are validated nowhere — a typo reproduces the exact 401 this PR fixes, silently.
Verified: no code outside this mapping touches auth_scheme — no validate-package check, no warning. auth_scheme: apikey_headr parses fine, matches no branch, degrades to native-headers-only → every call against Kong fails with No API key found in request (the precise #302 symptom) with zero diagnostics pointing at the typo. Same failure class as #297's capability-name typo, which got parse-time validation — the precedent exists, and this PR's new constants make the check trivial: validate against the known set ("", x_api_key, bearer, aws_sigv4, apikey_header) here (warn) or in forge validate (error).
A warn for unsupported provider+scheme combos fits the same check: gemini/ollama + apikey_header is silently ignored today (only the openai/anthropic clients are wired).
| // scheme (issue #302). Empty → "apikey" (Kong key-auth's default | ||
| // key_names). Set it for a gateway with custom key_names, e.g. | ||
| // "x-gateway-key". Ignored for every other AuthScheme. | ||
| AuthHeaderName string `yaml:"auth_header_name,omitempty"` |
There was a problem hiding this comment.
Minor: fallbacks can't use this scheme — worth one documenting sentence.
ModelFallback carries only provider/name/base_url, and resolveFallbacks builds bare ClientConfig{APIKey, Model} — no AuthScheme/AuthHeaderName. A fallback routed through the same Kong gateway (a base_url on the fallback entry) 401s exactly like the primary did before this PR. The shape is pre-existing (#202's aws_sigv4 has the same gap), but this PR's docs actively encourage gateway-fronted deployments — so either add the two fields to ModelFallback (small, symmetric) or state the limitation in the docs: "auth_scheme applies to the primary model only."
| if headerName == "" { | ||
| headerName = llm.DefaultAPIKeyHeaderName | ||
| } | ||
| req.Header.Set(headerName, apiKey) |
There was a problem hiding this comment.
Minor (security): guard against auth_header_name colliding with a native auth header.
This runs AFTER the provider's native header set and uses Header.Set — so auth_header_name: Authorization silently overwrites the native Bearer header with the raw key (no prefix), breaking auth in a maximally confusing way; auth_header_name: x-api-key on Anthropic is a harmless same-value overwrite but equally nonsensical. Reject or warn when the custom name case-insensitively equals Authorization or x-api-key — fits naturally into the scheme validation suggested on config.go.
|
|
||
| Some API gateways authenticate with a fixed header name rather than the provider-native scheme. Kong AI Gateway's `key-auth` plugin, for example, reads the consumer key from an `apikey` header and ignores `Authorization` / `x-api-key`, so an agent pointed at a Kong-fronted `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL` otherwise fails every LLM call with `401 {"message":"No API key found in request"}`. | ||
|
|
||
| `model.auth_scheme: apikey_header` sends the API key in the gateway header **in addition to** the provider-native header — additive, so it stays safe against non-gateway endpoints (the gateway consumes `apikey`; Kong's ai-proxy replaces/injects the upstream provider header). The header name defaults to `apikey` (Kong `key-auth`'s default `key_names`) and is overridable via `auth_header_name` for gateways with custom key names: |
There was a problem hiding this comment.
Minor (docs): worth one sentence on upstream key transit — Kong key-auth defaults to hide_credentials: false, meaning the gateway forwards the apikey header upstream, so the credential transits in a header conventional redaction tooling (which knows Authorization/x-api-key) won't scrub. Recommend hide_credentials: true on the Kong plugin here. (Forge's own tracing redactor matches key values by shape, so opt-in content capture is already covered.)
Address the PR #303 review's three minors: 1. auth_scheme is now validated (forge-core/validate/forge_config.go): an unrecognized value errors (a typo previously degraded silently to native-headers-only, reproducing the exact 401 apikey_header fixes), auth_scheme on a non-openai/anthropic provider warns (only those clients honor it), and a stray auth_header_name warns. 3. Header collision: an auth_header_name that case-insensitively equals a native auth header (Authorization / x-api-key) errors in validation, and setGatewayAPIKeyHeader refuses to overwrite a native header as defense-in-depth for the programmatic path. 2. Fallbacks: documented that auth_scheme applies to the PRIMARY model only (ModelFallback godoc + schema doc + knowledge skill) — matches the pre-existing aws_sigv4 shape; per-fallback fields tracked as a follow-up (the FORGE_MODEL_FALLBACKS env source needs a parallel encoding). 4. Docs: note Kong key-auth hide_credentials: false forwards the apikey header upstream; recommend hide_credentials: true. Tests cover unknown scheme, collision (incl. lowercase), provider-combo and stray-header warnings, and the runtime no-clobber guard.
|
Thanks — all three addressed in 1 (recommended ride-along) —
3 (security) — header collision. An 2 (fallbacks) — documented the limitation. 4 (docs) — Kong upstream key transit. Added the Tests: unknown scheme, collision (incl. lowercase), provider-combo + stray-header warnings, and the runtime no-clobber guard. Full |
initializ-mk
left a comment
There was a problem hiding this comment.
Re-review of 8e262f2 — all findings resolved; one loose end (file the promised follow-up issue)
Finding 1 (validation) — fixed beyond the ask. ValidateForgeConfig now carries four checks: unknown auth_scheme → error naming the known set (built from this PR's own llm constants, so it can't drift), scheme on an unsupported provider → warning (covering the gemini/ollama silent-ignore for both aws_sigv4 and apikey_header), native-header collision → error, and a stray auth_header_name without the scheme → warning — that last one wasn't even asked for and catches the "set the header but forgot the scheme" half-configuration. All five cases table-tested, including the literal apikey_headr typo from the review.
Finding 3a (collision) — belt and braces. The validation error is backed by defense-in-depth in setGatewayAPIKeyHeader itself (case-insensitive refusal to touch authorization/x-api-key), protecting programmatic ClientConfig construction that never passes through forge validate. The test pins the lowercase variant and asserts the native Bearer survives.
Finding 3b — docs note added recommending hide_credentials: true on the Kong plugin.
Finding 2 (fallbacks) — documented as a limitation with an honest deferral reason (the FORGE_MODEL_FALLBACKS env source needs a parallel encoding first): godoc, schema doc, and knowledge skill all state auth_scheme applies to the primary model only. Reasonable decline-with-tracking — except the tracking doesn't exist yet: the commit says "tracked as a follow-up," but no such issue is in the tracker (checked). Please file it so the commit message is true.
Verdict
All findings resolved — merge-ready once the in-flight Lint/Test checks go green and the per-fallback auth_scheme follow-up issue is filed (a one-minute action).
Closes #302.
Problem
Kong AI Gateway's
key-authplugin reads the consumer key from a fixedapikeyheader (its defaultkey_names) and ignores provider-native auth headers. Forge's LLM clients send only the provider-native scheme —Authorization: Bearer <key>(OpenAI-shape) orx-api-key: <key>(Anthropic-shape) — so an agent pointed at a Kong-frontedOPENAI_BASE_URL/ANTHROPIC_BASE_URLfails every LLM call:The platform's own gateway clients were already fixed to send the key in both headers; agents were the remaining consumer, and the header logic lives here in forge's provider clients.
Change
Extend the existing
AuthSchemeextension point (added foraws_sigv4in #202) with a new scheme:auth_scheme: apikey_header— sends the API key in a gateway header in addition to the provider-native header. Additive by design: the gateway consumesapikey, Kong's ai-proxy replaces/injects the upstream provider header, and OpenAI-compatible endpoints ignore the extra header — so the scheme is safe to enable against non-Kong gateways too.auth_header_name(optional) — overrides the header name for gateways with customkey_names(e.g.x-gateway-key). Defaults toapikey.The key comes from the usual
OPENAI_API_KEY/ANTHROPIC_API_KEYenv var.Implementation
llm.ClientConfig/types.ModelRef: newAuthHeaderNamefield; introduced scheme constants (AuthSchemeAWSSigV4/AuthSchemeAPIKeyHeader/DefaultAPIKeyHeaderName) that also replace the scattered"aws_sigv4"literals.providers/auth_scheme.go: sharedsetGatewayAPIKeyHeaderhelper, wired into both providers'setHeaders— additive, and a no-op off-path or whenAPIKeyis empty.runtime/config.go: carriesModelRef.auth_header_nameonto the client config (mirrors theauth_scheme/aws_regioncarry-over).Tests
apikeyheader underapikey_header.auth_header_nameoverride is honored (and the defaultapikeyheader isn't also set).ResolveModelConfigcarriesauth_scheme+auth_header_namefromforge.yamlonto the client.All pass with the full
forge-coresuite;gofmt+golangci-lintclean.Docs
docs/core-concepts/runtime-engine.md,docs/reference/forge-yaml-schema.md, and the knowledge skill document the scheme, the additive semantics, and theauth_header_nameoverride.Follow-up (not in this PR)
Platform wiring in agent-builder — an env (e.g.
AI_ROUTER_AUTH_SCHEME=apikey_header) written into the generatedforge.yamlmodel block at build, so operators configure it once per install — matching the SigV4 precedent. Tracked separately.