sequenceDiagram
participant FE as Frontend
participant GW as Gateway (Envoy / nginx)
participant AC as access-control-service
participant AS as agent-service
FE->>GW: /api/agents/** — Bearer JWT (or ?access-token for the WS)
GW->>AC: ext_authz → /api/auth (forwards original path + token)
alt valid JWT, REGULAR/ADMIN
AC-->>GW: 200 + x-user-id / x-user-name / x-user-email
GW->>AS: forward request (JWT preserved)
Note over AS: re-verifies the JWT itself (HS256, secret from auth.conf)
AS-->>FE: response
else missing / invalid / wrong role
AC-->>GW: 401 / 403
GW-->>FE: blocked (never reaches agent-service)
end
Feature Summary
The
agent-servicecurrently authenticates its own requests, and the check is both weak and incomplete:userTokeninPOST /api/agents.agent-service/src/api/auth-api.tsbase64-decodes the JWT payload and checks only theexpclaim — the signature is never verified (validateToken = !isTokenExpired), so a forged-but-unexpired token passes.GET /api/agents,GET /api/agents/models,PATCH …/settings, and the/api/agents/:id/reactWebSocket that drives LLM calls (and provider spend) — performs no auth at all.SecurityPolicy(bin/k8s/templates/gateway-security-policy.yaml) targets only the*-dynamic-routesHTTPRoute (wsapi / executions-stats / pve). The*-agent-service-routeis not targeted, so/api/agentsbypasses ext-authz entirely. (Single-node nginx has no ext-authz at all.)Proposed Solution or Design
Authorize requests via
access-control-serviceSecurityPolicytargeting the*-agent-service-routeHTTPRoute, mirroringgateway-security-policy.yaml, pointing ext-auth at the access-control-service/api/authand injectingx-user-id/x-user-name/x-user-email.AccessControlResource.authorize()for/api/agents…that verifies the JWT (parseToken) and requiresREGULAR/ADMIN(SessionUser.isRoleOf) — i.e. restore the gate the old LiteLLM proxy had — returning the trusted identity headers on success and 401/403 otherwise. Any valid REGULAR/ADMIN user is allowed on any agent path (allow-all per-agent for now). Token extraction already supports headerAuthorization: Bearer, queryaccess-token(for the WS), and bodytoken.agentHeaders()only setsX-Agent-Workflow-Id,fetchModelTypes()sends nothing, and the/reactWebSocket carries no token. REST →Authorization: Bearer; WS →?access-token=…(mirror the workflow WebSocket).auth.conf(envAUTH_JWT_SECRET), replacing the old non-cryptographic decode. This runs in addition to the gateway ext-authz, as defense-in-depth so direct / bare-metal access is authenticated too.auth_requestsubrequest to/api/auth(Envoy and nginx both forward the original path so the sameauthorize()branch handles them).sequenceDiagram participant FE as Frontend participant GW as Gateway (Envoy / nginx) participant AC as access-control-service participant AS as agent-service FE->>GW: /api/agents/** — Bearer JWT (or ?access-token for the WS) GW->>AC: ext_authz → /api/auth (forwards original path + token) alt valid JWT, REGULAR/ADMIN AC-->>GW: 200 + x-user-id / x-user-name / x-user-email GW->>AS: forward request (JWT preserved) Note over AS: re-verifies the JWT itself (HS256, secret from auth.conf) AS-->>FE: response else missing / invalid / wrong role AC-->>GW: 401 / 403 GW-->>FE: blocked (never reaches agent-service) end