Zero-trust API gateway for the OmniBioAI platform.
Gateway for the configured OmniBioAI backend services. Proxied requests pass
through the gateway middleware chain for authentication, policy evaluation,
compute quota checks where applicable, and audit emission before forwarding.
The gateway is not the implementation of the backend services and its route
map is limited to the services configured in app/core/router.py.
Internet / Client / Studio
↓
api-gateway :8080 ← single entry point
↓
TraceMiddleware ← generates X-Trace-Id UUID
↓
AuthMiddleware ← JWT validation via IAM client + Redis cache
↓
PolicyMiddleware ← RBAC/ABAC via policy-engine
↓
HPCMiddleware ← GPU/CPU quota via hpc-policy-engine (compute paths only)
↓
AuditMiddleware ← async audit log via security-audit
↓
target service ← workbench / tes / toolserver / model-registry / rag
Failure policy:
| Layer | On failure |
|---|---|
| Auth | FAIL CLOSED → HTTP 401 |
| Policy | FAIL CLOSED → HTTP 403 |
| HPC quota | FAIL CLOSED → HTTP 403 |
| Audit | FAIL OPEN → ignored |
Every request that isn't /health or / passes through this exact chain
(app/main.py; middleware is registered LIFO, so TraceMiddleware —
added last — runs first):
Incoming JWT
│ Authorization: Bearer <jwt>
▼
IAM validation AuthMiddleware → IAMClient.validate(token)
│ — Redis-cached (key "iam:{token}", TTL 300s) first,
│ else POST {IAM_URL}/auth/validate (1 retry on timeout)
│ — no token or a rejected token → 401, fail closed
▼
Policy PolicyMiddleware → PolicyClient.evaluate(...)
│ — POSTs user_id/email/roles/permissions/action/resource
│ to {POLICY_URL}/policy/evaluate
│ — `action` is the target service's mapped IAM
│ permission when one exists (SERVICE_PERMISSION_MAP
│ in app/core/router.py: workbench/tes/toolserver →
│ workflow.execute, model-registry → model.use,
│ rag → dataset.read), else a derived
│ "{method}.{path}" string — this mapping only
│ enriches what the policy engine evaluates against;
│ the engine's remote decision is still the sole
│ allow/deny authority
│ — deny → 403, fail closed
│ (HPCMiddleware runs next, compute paths only —
│ GPU/CPU quota via {HPC_URL}/jobs/evaluate, also
│ fail-closed)
▼
Forward Authorization header
│ gateway.py rebuilds the outgoing header set from
│ scratch (never re-forwards the raw incoming
│ "authorization" key) and sets
│ Authorization: Bearer <token> from
│ request.state.token — the exact string
│ AuthMiddleware already validated, not a re-parse
│ of the original header — so the backend service
│ can independently re-verify identity itself.
▼
Backend services proxy.forward() to the resolved SERVICE_MAP target
(workbench / tes / toolserver / model-registry / rag)
The gateway never decodes a JWT itself — no local HS256/RS256 verification,
no JWKS client. Every validation is a remote call to auth-service's
/auth/validate (Redis-cached), delegating entirely to the shared identity
layer described in omnibioai-auth's README rather
than duplicating its verification logic.
Alongside the re-forwarded Authorization header, the gateway also injects:
| Header | Value | Notes |
|---|---|---|
X-User-Id |
The authenticated user's ID | Unsigned — a convenience header for services that don't want to re-decode the JWT; the re-forwarded Authorization bearer token is the only header a backend should actually trust for authorization decisions |
X-Internal-Service |
gateway |
Marks the request as gateway-verified/internal |
org_id/org_role are resolved into request.state.user during IAM
validation but are not propagated as their own headers today — a
backend that needs them re-derives them from the forwarded JWT itself
(/auth/validate or a local jwt_verify.py, same as Control Center and
Security Audit do).
See "Forward Authorization header" in the pipeline above — this is not a service-to-service token, it is the caller's own validated bearer token, forwarded as-is so the backend service can verify it independently rather than trusting the gateway's decision blindly.
TraceMiddleware (outermost — runs first) generates or echoes an
X-Trace-Id UUID, attaches it to request.state.trace_id, sets it on the
response, and it's propagated on to the backend request. Every
middleware's own audit events (auth/policy/HPC denials, the final
upstream-forward event) carry this same trace ID, so a single request can
be followed end-to-end through the audit stream.
There is currently no separate service-to-service credential (no service
JWT, no mTLS, no API key) — the gateway calls backend services carrying
only the unsigned X-Internal-Service: gateway marker plus the forwarded
user bearer token. Backend-to-backend trust within the compose network is
not independently authenticated at the gateway layer; each backend service
is expected to validate the forwarded JWT itself if it needs a real
identity guarantee.
- JWT authentication via IAM client (Redis-cached, sub-ms validation)
- RBAC/ABAC policy enforcement on every request
- GPU/CPU quota governance for compute requests
- Async audit logging via Redis Streams (never blocks requests)
- Original bearer-token forwarding to backend services (see Authentication Pipeline)
- Distributed trace ID propagation (X-Trace-Id header)
- Redis pub/sub cache invalidation on logout
- Rate limiting on auth endpoints (via nginx — 10 req/min, burst 5)
Middleware is applied LIFO — last added runs first for requests:
| Order | Middleware | Responsibility |
|---|---|---|
| 1 | TraceMiddleware | Generate X-Trace-Id, attach to request state |
| 2 | AuthMiddleware | Validate JWT via IAM client |
| 3 | PolicyMiddleware | RBAC/ABAC authorization decision |
| 4 | HPCMiddleware | GPU/CPU quota check (compute paths only) |
| 5 | AuditMiddleware | Fire async audit event to Redis Streams |
app/
├── main.py # Registers middleware (LIFO) and routers
├── core/
│ ├── config.py # Settings from environment
│ ├── router.py # SERVICE_MAP, SERVICE_PERMISSION_MAP,
│ │ # resolve_service(), resolve_required_permission()
│ ├── permissions.py # require_permission() dependency — for
│ │ # gateway-native routes only (e.g. /auth/verify),
│ │ # deliberately not wired into the proxy route
│ │ # to avoid duplicating PolicyMiddleware's decision
│ └── security.py # Trace-ID generation + its own audit event
├── middleware/
│ ├── s2s.py # TraceMiddleware (name predates current scope —
│ │ # this is not a service-to-service credential
│ │ # system; see "Service identity" above)
│ ├── auth.py # AuthMiddleware
│ ├── policy.py # PolicyMiddleware
│ ├── hpc.py # HPCMiddleware
│ └── audit.py # AuditMiddleware
├── routes/
│ ├── auth_verify.py # GET /auth/verify
│ └── gateway.py # Catch-all proxy route
└── services/
├── iam_client.py # IAM validation client
├── policy_client.py # Policy engine client
├── hpc_policy_client.py # HPC policy engine client
└── audit_client.py # Audit event client
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/health |
GET | — | Gateway health check |
/auth/verify |
GET | JWT | Verify token (used by nginx auth_request) |
/{service}/{path} |
GET/POST/PUT/DELETE | JWT | Proxy to target service — no /api prefix |
curl http://localhost:8080/health
# {"status": "ok"}curl -H "Authorization: Bearer <token>" \
http://localhost:8080/tes/jobs| Path | Target service |
|---|---|
/workbench/* |
workbench :8000 |
/tes/* |
tes :8081 |
/toolserver/* |
toolserver :9090 |
/model-registry/* |
model-registry :8095 |
/rag/* |
rag :8096 |
LIMS is not proxied through this gateway's SERVICE_MAP — it's reached
via a separate route.
| Header | Description |
|---|---|
Authorization |
The caller's original bearer token, re-forwarded verbatim (see Authentication Pipeline) |
X-Trace-Id |
UUID per request for distributed tracing |
X-User-Id |
Authenticated user ID (unsigned convenience header) |
X-Internal-Service |
Marks request as internal (gateway-verified) |
cd ~/Desktop/machine/omnibioai-studio
docker compose up -d api-gatewayAccess: http://localhost:8080
Set in omnibioai-studio/.env:
| Variable | Default | Description |
|---|---|---|
IAM_URL |
http://omnibioai-auth:8000 |
Auth service URL |
POLICY_URL |
http://omnibioai-policy-engine:8001 |
Policy engine URL |
HPC_URL |
http://omnibioai-hpc-policy-engine:8002 |
HPC policy URL |
REDIS_URL |
redis://redis:6379 |
Redis for token cache + pub/sub |
JWT_SECRET |
— | JWT signing secret (auto-generated) |
ROUTE_TIMEOUT |
15 |
Upstream request timeout (seconds) |
cd ~/Desktop/machine/omnibioai-api-gateway
pytest tests/ -v --cov=app
# The command above is the supported validation entrypoint. Test counts and
# coverage figures from earlier dated runs are historical snapshots and are
# not asserted as current status here.
# It covers auth middleware, policy middleware, HPC middleware,
# trace middleware, audit middleware, config, gateway router,
# permissions dependency, and the PR12/PR13 middleware-chain
# end-to-end + org-context/role-tier forwarding suites| Layer | Technology |
|---|---|
| Framework | FastAPI + Uvicorn |
| Auth | IAM client (httpx async + Redis cache) |
| Cache invalidation | Redis pub/sub |
| Tracing | UUID trace IDs via middleware |
| Proxying | httpx reverse proxy |
| Service | Role |
|---|---|
omnibioai-auth |
JWT issuance and validation |
omnibioai-iam-client |
Async IAM client with Redis cache |
omnibioai-policy-engine |
RBAC/ABAC authorization decisions |
omnibioai-hpc-policy-engine |
GPU/CPU quota governance |
omnibioai-security-audit |
Async audit event consumer |
omnibioai-security-sdk |
SDK wrapping the full security stack |
omnibioai-studio |
Manages gateway container lifecycle |
Apache 2.0
Part of the OmniBioAI platform.