Clef-flash is a decision model, not a chat model. It serves Clef's own
System One / Jev API at /v1/systemone: a state plus a schema of typed
questions in, one probability per allowed option out, single prefill
pass, no decoding and no free-form text.
Router previously dispatched by body `model` only for
/v1/chat/completions, /v1/embeddings and /v1/rerank. A request to
/v1/systemone therefore fell through to the configured routes, found
none, and answered "no routes configured". Adding the path to the
dispatch predicate is the whole fix: routeByModel already sets
upstreamPath := path, so the upstream receives /v1/systemone unchanged.
There is deliberately no /rerank-style rewrite.
Config: `ornith:35b` is removed with the Ollama predictor, and its GPU
now serves clef-flash. `qwen2.5:3b-instruct` already pointed at qwen-cpu
and is unchanged. Callers of ornith:35b must move to `reasoning`, which
keeps its serving name while running Qwen3-30B-A3B.
Tests cover body dispatch, unknown_model parity with the other three
paths, and that the upstream path is not rewritten.
The service CIDR is 10.106.0.0/16, so the ipBlock 10.96.0.0/12 never covered
ingress-nginx (10.101.128.185). The JWKS fetch to zitadel.riotpiao.com therefore
hung until the context deadline: every caller got 403, and the pod failed its
liveness probe before serving traffic.
Add an egress rule for TCP 443 to the ingress-nginx namespace and point jwksUrl
back at the public host. The ZITADEL service port 8080 answers 404 for
/oauth/v2/keys, so the service endpoint is not usable.
The public host resolves through the CoreDNS rewrite to ingress-nginx and the
fetch timed out, so the validator answered 403 with a context-deadline error on
every caller. Point jwksUrl at zitadel.iam.svc.cluster.local:8080, which the
NetworkPolicy already allows.
cmd/gateway/main.go:144 constructs the adapter validator only when both
cfg.Auth.Enabled and cfg.Auth.JWKSURL are set. The config left jwksUrl unset on
purpose, so jwtValidator stayed nil and every adapter request passed with one
warning line:
auth required but jwtValidator nil; request unauthenticated
That made auth.required cosmetic: sqs, workflow, memory, s3 and iam all answered
unauthenticated calls. Set the JWKS URL to the ZITADEL key endpoint.
workflow, memory, s3 and iam served requests without any Authorization header.
Only sqs and the LLM models checked a token, so four upstreams were reachable
from inside the cluster with no authentication at all.
Set auth.required true on all five adapters. The dispatcher then enforces one
capability per method, derived from the verb
(internal/serviceadapter/router.go:142):
GET/HEAD/OPTIONS -> "<service>:read"
anything else -> "<service>:write"
Those strings must exist as ZITADEL role keys with the colon. The project now
holds memory:read, memory:write, workflow:read, workflow:execute, s3:read,
s3:write, iam:read, iam:write, sqs:read and sqs:write, and the riotpiao user
grant carries them, so a browser session keeps working.
Correct the old comment: ZITADEL does store colon role keys. Verified by listing
the project roles, which returned all eleven colon keys.
Replace the handwritten Authentik JWT checks with github.com/zitadel/zitadel-go/v3.
What the SDK now owns:
- OIDC discovery (client.Discover) to find issuer and JWKS URI
- remote, auto-refreshing JWKS (rp.NewRemoteKeySet)
- signature, expiry and issuer checks (op.NewAccessTokenVerifier,
op.VerifyAccessToken)
- the verified claim set and role lookup (oauth.IntrospectionContext,
IsGrantedRole)
What stays local:
- a multi-audience allow-list, because this gateway is a shared resource
server and the SDK's oauth.WithJWT accepts exactly one client ID
- an adapter from IntrospectionContext to jwt.MapClaims so the rest of the
gateway keeps one claim representation
Other changes:
- issuer is configuration-driven; the hardcoded authentik.riotpiao.com
check is gone
- aud may be a string or an array (ZITADEL sends an array)
- CheckPermissions also reads urn:zitadel:iam:org:project:roles, the claim
ZITADEL uses for project roles
- requiredCapability accepts a comma-separated list, so llm:inference and
llm-inference both satisfy the LLM gate
- a five-segment token is reported as an encrypted JWE with the fix in the
message, instead of failing as a bad signature
Config: auth.zitadelDomain turns the SDK path on; jwksUrl stays supported for
the legacy path and for tests.
- Remove SOPS-encrypted secret file (was causing pod init failures)
- Use plaintext decrypted secret (mounted via kubernetes secret mechanism)
- Update kustomization to reference decrypted secret file
- All sensitive values remain protected by SOPS in git history
- Pods can now reliably decrypt and load config during initialization