- 1. Architecture Overview
- 2. Principles & Constraints
- 3. Technical Architecture
- 4. Additional Context
- 5. Traceability
OAGW is a centralized outbound API gateway that manages all outbound API requests from Cyber Ware to external services. It provides routing, authentication, rate limiting, and monitoring through a unified proxy layer that enforces security and observability policies.
The architecture follows a Control Plane / Data Plane separation within a single module, where the Control Plane manages configuration data (upstreams, routes, plugins) and the Data Plane orchestrates proxy requests to external services. Both services are implemented as domain traits within a single oagw crate, using DDD-Light layering (domain/infra/api).
This design satisfies the requirements for centralized outbound traffic management, multi-tenant hierarchical configuration, and extensible plugin-based request processing while remaining practical to implement within Cyber Ware's modular monolith architecture.
ID: cpt-cf-oagw-design-drivers
| Driver | Source | Influence |
|---|---|---|
| Centralized outbound traffic management | cpt-cf-oagw-fr-request-proxy |
Single proxy endpoint for all external API calls |
| Multi-tenant hierarchy | cpt-cf-oagw-fr-hierarchical-config |
Configuration sharing/inheritance across tenant tree |
| Plugin extensibility | cpt-cf-oagw-fr-plugin-system |
Three plugin types (Auth/Guard/Transform) with trait-based isolation |
| Low-latency proxy path | cpt-cf-oagw-nfr-low-latency |
In-memory rate limiters |
| Configuration layering | cpt-cf-oagw-fr-config-layering |
Upstream < Route < Tenant merge priority |
| Alias resolution | cpt-cf-oagw-fr-alias-resolution |
Path-based routing with alias shadowing |
| Credential isolation | cpt-cf-oagw-nfr-credential-isolation |
Auth via cred_store references, no direct secret storage |
| ModKit integration | Cyber Ware middleware | Single-executable deployment, trait-based DI, secure ORM |
Architecture Decision Records:
cpt-cf-oagw-adr-component-architecture— Single module with internal trait-based service isolationcpt-cf-oagw-adr-request-routing— Path-based routing with alias resolutioncpt-cf-oagw-adr-plugin-system— Three plugin types with deterministic execution ordercpt-cf-oagw-adr-rate-limiting— Token bucket algorithm with hierarchical mergecpt-cf-oagw-adr-circuit-breaker— State machine with configurable thresholds and fallbackcpt-cf-oagw-adr-cors— Built-in CORS handler with per-upstream/route configcpt-cf-oagw-adr-data-plane-caching— Control plane caching strategiescpt-cf-oagw-adr-state-management— CP/DP state structures with cache invalidationcpt-cf-oagw-adr-storage-schema— Cross-database relational schema with JSON config blobscpt-cf-oagw-adr-resource-identification— Layered resource model with alias resolutioncpt-cf-oagw-adr-concurrency-control— Per-scope in-flight request limitscpt-cf-oagw-adr-backpressure-queueing— Bounded queueing with degradation strategiescpt-cf-oagw-adr-error-source-distinction— Response header for gateway vs upstream errorscpt-cf-oagw-adr-grpc-support— HTTP/2 multiplexing with protocol detectioncpt-cf-oagw-adr-rust-abi-client-library— Rust ABI client for internal module routing
ID: cpt-cf-oagw-design-layers
| Layer | Responsibility | Key Components |
|---|---|---|
Transport (api/rest/) |
HTTP handling, request parsing, response serialization | Axum handlers, DTOs, extractors, OperationBuilder route registration |
Domain (domain/) |
Business logic, service traits, repository contracts | ControlPlaneService, DataPlaneService, AuthPlugin trait, domain models |
Infrastructure (infra/) |
External integrations, persistence, proxy engine | SeaORM repositories, Pingora proxy bridge, plugin registry, type provisioning |
SDK (oagw-sdk/) |
Public API for inter-module communication | ServiceGatewayClientV1 trait, SDK models, error types |
The module follows DDD-Light layering: domain layer has no infrastructure dependencies; infrastructure implements domain traits; transport layer maps between HTTP and domain types.
ID: cpt-cf-oagw-tech-dependencies
| Technology | Purpose |
|---|---|
| Rust / Axum | HTTP transport, async runtime |
SeaORM + modkit-db |
Database persistence (PostgreSQL, MySQL, SQLite) |
| Pingora | Reverse-proxy engine (connection pooling, HTTP/2, load balancing) |
| Starlark | Custom plugin sandboxed execution |
modkit-auth |
Bearer token authentication & authorization |
cred_store |
Secret material retrieval by URI reference |
types_registry |
GTS schema/instance registration |
ID: cpt-cf-oagw-design-overview
graph TB
Client[Client / Internal Module] -->|Management API| APIHandler[API Handler]
Client -->|Proxy API| APIHandler
APIHandler -->|CRUD ops| CP[Control Plane<br/>ControlPlaneService]
APIHandler -->|Proxy ops| DP[Data Plane<br/>DataPlaneService]
DP -->|resolve config| CP
CP -->|read/write| DB[(Database)]
DP -->|auth plugin| AuthPlugin[Auth Plugins]
DP -->|guard/transform| PluginChain[Plugin Chain]
DP -->|HTTP call| Upstream[External Services]
CP -->|secrets| CredStore[cred_store]
CP -->|types| TypesRegistry[types_registry]
ID: cpt-cf-oagw-principle-no-retry
No automatic retries: OAGW never retries failed upstream requests. Retry logic is the client's responsibility.
ID: cpt-cf-oagw-principle-no-cache
No response caching: OAGW does not cache upstream responses. Caching is client/upstream responsibility.
ID: cpt-cf-oagw-principle-cred-isolation
Credential isolation: OAGW references secrets via cred_store URIs (cred://...); never stores or logs secret material.
ID: cpt-cf-oagw-principle-tenant-scope
Tenant scoping: All database reads/writes use secure ORM with tenant scoping. No raw SQL in module code.
ID: cpt-cf-oagw-principle-plugin-immutable
Plugin immutability: Custom plugins are immutable after creation. Updates create new versions.
ID: cpt-cf-oagw-principle-error-source
Error source distinction: All error responses include X-OAGW-Error-Source: gateway|upstream header.
ID: cpt-cf-oagw-principle-rfc9457
RFC 9457 Problem Details: All gateway errors use application/problem+json format with GTS type identifiers.
ID: cpt-cf-oagw-constraint-modkit-deploy
Single-executable deployment via ModKit (Cyber Ware middleware requirement).
ID: cpt-cf-oagw-constraint-no-direct-internet
No direct internet access from internal modules. Security policy: all outbound traffic routes through OAGW.
ID: cpt-cf-oagw-constraint-multi-sql
Multi-SQL backend portability: PostgreSQL, MySQL, SQLite. modkit-db requirement; avoid backend-specific features for correctness.
ID: cpt-cf-oagw-constraint-body-limit
Body size hard limit: 100MB. Prevent resource exhaustion; reject before buffering.
ID: cpt-cf-oagw-constraint-https-only
HTTPS-only for upstream connections in MVP. SSRF prevention; plaintext upstreams explicitly blocked.
ID: cpt-cf-oagw-design-domain-model
classDiagram
class Upstream {
+UUID id
+UUID tenant_id
+String alias
+String protocol
+Boolean enabled
+ServerConfig server
+AuthConfig auth
+HeadersConfig headers
+RateLimitConfig rate_limit
+CorsConfig cors
+PluginsConfig plugins
+List~String~ tags
}
class Route {
+UUID id
+UUID tenant_id
+UUID upstream_id
+String match_type
+Int priority
+Boolean enabled
+MatchConfig match
+RateLimitConfig rate_limit
+CorsConfig cors
+PluginsConfig plugins
+List~String~ tags
}
class Plugin {
+UUID id
+UUID tenant_id
+String plugin_type
+String name
+JsonSchema config_schema
+String source_code
+Timestamp last_used_at
+Timestamp gc_eligible_at
}
class ServerConfig {
+List~Endpoint~ endpoints
}
class Endpoint {
+String scheme
+String host
+Int port
}
Upstream "1" --> "*" Route : has routes
Upstream "1" --> "0..1" Plugin : auth plugin
Upstream "1" --> "*" Plugin : guard/transform plugins
Route "1" --> "*" Plugin : guard/transform plugins
ServerConfig "1" --> "1..*" Endpoint : endpoints
Upstream "1" --> "1" ServerConfig : server
Key Domain Entities:
- Upstream (
gts.cf.core.oagw.upstream.v1~): Tenant-scoped root configuration object representing an external service. Unique per(tenant_id, alias). Contains server endpoints, auth config, rate limits, CORS, headers, and plugin bindings. - Route (
gts.cf.core.oagw.route.v1~): Belongs to an upstream. Defines match rules (HTTP path/method; gRPC service/method matching is planned for Phase 3 — no gRPC proxy code path is currently implemented or reachable), priority, and route-level overrides for rate limits, CORS, and plugins. - Plugin (
gts.cf.core.oagw.{type}_plugin.v1~): Custom tenant-defined Starlark plugins stored inoagw_plugin. Named (built-in) plugins are resolved via in-process registry and not persisted.
Base type: gts.cf.core.oagw.upstream.v1~
Full schema: schemas/upstream.v1.schema.json
Sharing fields fragment: schemas/oagw_upstream_sharing_fields.fragment.json
Base type: gts.cf.core.oagw.route.v1~
Full schema: schemas/route.v1.schema.json
Auth Plugin — Base type: gts.cf.core.oagw.auth_plugin.v1~ — schemas/auth_plugin.v1.schema.json
One per upstream. Named auth plugins resolved via in-process registries, not stored in oagw_plugin. UUID-backed auth plugins stored in oagw_plugin.
Built-in auth plugins:
gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.noop.v1gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.basic.v1gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.bearer.v1gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.oauth2_client_cred.v1gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.oauth2_client_cred_basic.v1
Guard Plugin — Base type: gts.cf.core.oagw.guard_plugin.v1~ — schemas/guard_plugin.v1.schema.json
Multiple per upstream/route. Can reject requests before they reach upstream.
| Plugin ID | Description |
|---|---|
gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.timeout.v1 |
Request timeout enforcement |
gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.cors.v1 |
CORS origin validation (actual requests; preflight handled at handler level — see ADR: CORS) |
Circuit breaker is core functionality (not a plugin). See ADR: Circuit Breaker.
Transform Plugin — Base type: gts.cf.core.oagw.transform_plugin.v1~ — schemas/transform_plugin.v1.schema.json
Multiple per upstream/route, executed in order. Each plugin declares supported phases: on_request, on_response, on_error.
| Plugin ID | Phase | Description |
|---|---|---|
gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.logging.v1 |
request, response, error | Request/response logging |
gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.metrics.v1 |
request, response | Prometheus metrics |
gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.request_id.v1 |
request, response | X-Request-ID injection/propagation |
All plugins are identified using GTS identifiers in the API layer.
The database stores:
plugin_ref(TEXT): the canonical plugin identifier string (full GTS identifier).plugin_uuid(UUID, nullable): extracted UUID whenplugin_refis UUID-backed.
Named plugins (built-in or provided by deployed modules):
- API:
gts.cf.core.oagw.{type}_plugin.v1~cf.core.oagw.{name}.v1 - Resolved via an in-process plugin registry.
- Not stored in
oagw_pluginand not subject to GC.
Custom plugins (tenant-defined Starlark):
- API:
gts.cf.core.oagw.{type}_plugin.v1~{uuid} - Stored in
oagw_pluginkeyed byid = {uuid}.
Bindings:
oagw_upstream_plugin/oagw_route_pluginstore(position, plugin_ref, plugin_uuid, config).- No FK from binding rows to
oagw_pluginbecause named plugins do not have DB rows. - Application validates that
plugin_uuidmatchesplugin_refwhen present.
Auth plugin storage: Upstreams store auth plugin identity in scalar columns (auth_plugin_ref, auth_plugin_uuid) so "plugin in use" checks do not depend on JSON scanning.
Resolution Algorithm:
- Parse GTS identifier from API request to extract instance part (after
~) - If instance parses as UUID → resolve via
oagw_plugin(must exist and must match the plugin schema type) - Otherwise → resolve via in-process registry (named plugins)
- When persisting bindings, store
plugin_refalways; storeplugin_uuidonly for UUID-backed plugins
ID: cpt-cf-oagw-component-model
modules/system/oagw/
├── oagw-sdk/ # Public API: ServiceGatewayClientV1 trait, models, errors
│ └── src/
│ ├── lib.rs # Re-exports
│ ├── api.rs # ServiceGatewayClientV1 trait definition
│ ├── models.rs # SDK types (Upstream, Route, CreateUpstreamRequest, etc.)
│ └── error.rs # ServiceGatewayError
│
└── oagw/ # Single module crate with internal service isolation
└── src/
├── lib.rs # Public exports
├── module.rs # ModKit module wiring
├── config.rs # OagwConfig
├── api/rest/ # Transport layer
│ ├── handlers/ # Axum HTTP handlers (management + proxy)
│ ├── routes/ # OperationBuilder route registration
│ ├── dto.rs # REST DTOs (serde + utoipa)
│ ├── error.rs # Error response mapping
│ └── extractors.rs
├── domain/ # Business logic (no infra dependencies)
│ ├── services/ # ControlPlaneService + DataPlaneService traits & impls
│ ├── plugin/ # AuthPlugin trait definition
│ ├── dto.rs # Internal domain types (ProxyContext, ProxyResponse, etc.)
│ ├── repo.rs # Repository traits (UpstreamRepository, RouteRepository)
│ └── error.rs # DomainError
└── infra/ # Infrastructure implementations
├── proxy/ # DataPlaneServiceImpl (Pingora in-memory bridge)
├── storage/ # Repository impls (SeaORM-based)
├── plugin/ # AuthPluginRegistry + built-in plugins (ApiKey, NoOp)
└── type_provisioning.rs # GTS type registration
- ControlPlaneService (
domain/services/management.rs): Manages configuration data. Handles CRUD operations for upstreams/routes, alias resolution, and repository access. - DataPlaneService (
infra/proxy/service.rs): Orchestrates proxy requests. Resolves config via Control Plane, executes auth plugins, builds outbound HTTP requests, and forwards to upstream services.
Crate Naming: Directory names hyphenated (oagw-sdk), package names cf- prefixed (cyberware-oagw-sdk), library names underscored (oagw_sdk).
| Path Pattern | Routed To | Purpose |
|---|---|---|
/api/oagw/v1/upstreams/* |
Control Plane | Upstream CRUD |
/api/oagw/v1/routes/* |
Control Plane | Route CRUD |
/api/oagw/v1/plugins/* |
Control Plane | Plugin CRUD |
/api/oagw/v1/proxy/* |
Data Plane | Proxy requests |
Three plugin types with separate traits, deterministic execution order:
- AuthPlugin (
gts.cf.core.oagw.auth_plugin.v1~*): Credential injection. One per upstream. - GuardPlugin (
gts.cf.core.oagw.guard_plugin.v1~*): Validation/policy enforcement (can reject). Multiple per upstream/route. - TransformPlugin (
gts.cf.core.oagw.transform_plugin.v1~*): Request/response mutation. Multiple per upstream/route.
Execution order: Auth → Guards → Transform(on_request) → Upstream call → Transform(on_response/on_error).
Plugin chain composition: upstream plugins execute before route plugins ([U1, U2] + [R1, R2] => [U1, U2, R1, R2]).
Built-in Plugins:
- Auth:
noop,apikey,basic,bearer,oauth2_client_cred,oauth2_client_cred_basic - Guard:
timeout,cors - Transform:
logging,metrics,request_id
Custom Plugins: Starlark scripts with sandboxed execution (no network/file I/O, timeout/memory limits enforced). Immutable after creation; GC for unlinked plugins after configurable TTL.
Multi-tenant hierarchy with sharing modes (private/inherit/enforce) per configuration field:
| Field | Merge Strategy |
|---|---|
| Auth | Override if inherit; forced if enforce |
| Rate Limits | min(ancestor, descendant) — stricter always wins |
| Plugins | Concatenate: ancestor.plugins + descendant.plugins |
| CORS | Union origins if inherit; forced if enforce |
Tags do not have a sharing mode — they always use add-only union semantics: effective_tags = union(ancestor_tags, descendant_tags). Descendants cannot remove inherited tags.
Alias resolution walks tenant hierarchy from descendant to root; closest match wins (shadowing). Enforced ancestor constraints are never bypassed by shadowing.
Upstreams are identified by alias in proxy requests: {METHOD} /api/oagw/v1/proxy/{alias}/{path}.
Alias Enforcement Rules:
Alias behavior is determined entirely by endpoint type. The system enforces strict rules — aliases are not arbitrary labels.
| Endpoint Type | Alias Rule | Example |
|---|---|---|
| Hostname, standard port | Auto-derived (hostname) | api.openai.com:443 → api.openai.com |
| Hostname, non-standard port | Auto-derived (hostname:port) | api.openai.com:8443 → api.openai.com:8443 |
| Multiple hostnames, registrable common suffix (PSL-validated) | Auto-derived via common_domain_suffix() — shared suffix must be a registrable domain (≥2 labels, not a bare public suffix) |
us.vendor.com, eu.vendor.com → vendor.com |
| Multiple hostnames, common suffix is a bare public suffix | Explicit alias required (derivation rejected) | foo.co.uk, bar.co.uk → not derivable (co.uk is a public suffix) |
| Multiple hostnames, no registrable common suffix | Explicit alias required | us.foo.com, eu.bar.com → user must provide alias |
| IP addresses | Explicit alias required | 10.0.1.1, 10.0.1.2 → user provides my-service |
Hostname-based endpoints: alias is always auto-derived. User-provided alias that differs from the auto-derived value is rejected (400 Validation); providing the exact derived value is tolerated silently for idempotency.
IP-based or non-derivable endpoints: explicit alias is required from the user. Omitting the alias field returns 400 Validation.
Standard ports (omitted from derived alias): HTTP: 80, HTTPS/WSS/WebTransport/gRPC: 443.
Alias Normalization: All aliases are normalized to ASCII lowercase with trailing dots stripped. Resolution is case-insensitive (e.g., Api.OpenAI.COM resolves to an upstream with alias api.openai.com).
Hostname Validation: Endpoint hostnames are validated per RFC 1123: max 253 characters total, each label 1–63 characters, labels contain only ASCII alphanumeric and hyphen, labels cannot start or end with hyphen. A trailing dot (FQDN notation) is tolerated and stripped.
Alias Update Behavior:
| Transition | Behavior |
|---|---|
| Derivable → Derivable (new endpoints) | Alias recomputed from new endpoints |
| Derivable → Non-derivable | Rejected unless user provides explicit alias |
| Non-derivable → Non-derivable | Existing alias retained; user may provide a new one |
| Non-derivable → Derivable | Alias recomputed (old explicit alias replaced) |
| Derivable (no endpoint change) | Alias override rejected (400 Validation) |
| Non-derivable (no endpoint change) | User may update alias freely |
"Derivable" means compute_derived_alias() returns a value (single hostname, or multiple hostnames with a registrable common suffix). "Non-derivable" means derivation fails — this includes IP-based endpoints, heterogeneous hostnames with no common suffix, and hostname pools whose only common suffix is a bare public suffix (e.g., co.uk). See enforce_alias_update() for the full branching logic.
For multi-host endpoints with non-standard ports, the common suffix derivation preserves :port in the alias (e.g., us.vendor.com:8443 + eu.vendor.com:8443 → vendor.com:8443). This avoids collisions between pools sharing the same domain suffix on different ports — operators should reference the suffix:port form when routing to these upstreams.
Alias Uniqueness: Alias is unique per tenant, not globally. Database constraint: UNIQUE (tenant_id, alias). Tenants can independently manage upstreams without namespace collisions. Descendants can shadow ancestor aliases for controlled customization.
Shadowing Behavior: When resolving alias, OAGW walks tenant hierarchy from descendant to root. Closest match wins. Shadowing selects the routing target only — ancestor constraints configured with sharing: enforce remain active. Effective limits are computed with enforced ancestors included (e.g., effective_rate = min(selected_rate, route_rate, all_ancestor_enforced_rates)).
Multi-Endpoint Load Balancing: Multiple endpoints in the same upstream form a pool. Requests are distributed across endpoints (round-robin). All endpoints must have the same protocol, scheme, and port.
OAGW processes headers in three categories:
- Routing Headers: Consumed by OAGW during request routing and NOT forwarded to upstream services.
- Hop-by-Hop Headers: Stripped by default according to HTTP specifications.
- Passthrough Headers: Forwarded to upstream according to configuration rules.
| Inbound Header | Rule |
|---|---|
X-OAGW-Target-Host |
Read during routing, then stripped |
Host |
Replaced by upstream host |
Connection |
Stripped |
Keep-Alive |
Stripped |
Proxy-Authenticate |
Stripped |
Proxy-Authorization |
Stripped |
TE |
Stripped |
Trailer |
Stripped |
Transfer-Encoding |
Stripped |
Upgrade |
Stripped |
Simple header transformations are defined in the upstream headers configuration. Complex header transformations can be defined in corresponding upstream/route plugins. Well-known headers (e.g., Content-Length, Content-Type) must be validated, set or adjusted; invalid headers should result in 400 Bad Request.
HTTP/2 :authority Pseudo-Header and X-OAGW-Target-Host:
In HTTP/2, the :authority pseudo-header replaces the HTTP/1.1 Host header. OAGW's X-OAGW-Target-Host header behavior applies consistently across both protocols:
- HTTP/1.1:
X-OAGW-Target-Hostis used for routing; standardHostheader is replaced with upstream host - HTTP/2:
X-OAGW-Target-Hostis used for routing;:authoritypseudo-header is replaced with upstream authority - The
:authoritypseudo-header does NOT replaceX-OAGW-Target-Hostfor routing purposes. Both headers serve different functions.
Validation rules that can reject requests:
| Inbound | Rule |
|---|---|
| Method | Must be in match.http.methods; reject if not allowed |
| Query params | Validate against match.http.query_allowlist; reject if unknown |
| Path suffix | Reject if path_suffix_mode: disabled and suffix provided |
| Body | See body validation rules below |
| CORS origin | Reject if origin is not in upstream's allowed_origins (actual cross-origin requests only; preflight returns permissive 204 at handler level — see ADR: CORS) |
| CORS method | Reject if method is not in upstream's allowed_methods (actual cross-origin requests only) |
Default validation checks (no configuration required):
| Check | Rule | Error |
|---|---|---|
| Content-Length | Must be valid integer if present; must match actual size | 400 ValidationError |
| Max size | Hard limit 100MB; reject before buffering | 413 PayloadTooLarge |
| Transfer-Encoding | Reject unsupported encodings (only chunked supported) |
400 ValidationError |
Additional validation (JSON Schema, content-type checks, custom rules) implemented via guard plugins.
Rules that mutate inbound → outbound:
| Inbound | Outbound | Rule |
|---|---|---|
| Method | Method | Passthrough |
| Path suffix | Path | Append to match.http.path if path_suffix_mode: append; plugin mutable |
| Query params | Query | Passthrough allowed params; plugin mutable |
| Headers | Headers | Apply upstream.headers transformation rules; plugin mutable |
| Body | Body | Passthrough by default; plugin mutable |
Custom Plugins (Starlark):
- Immutable: Plugins cannot be updated after creation
- Versioning: Create new plugin for changes, update upstream/route references
- Deletion: Only unlinked plugins can be deleted
- Garbage Collection: Unlinked plugins are automatically deleted after TTL (default: 30 days). A periodic GC job marks plugins eligible by setting
gc_eligible_atwhen they become unlinked, and deletes plugin rows oncegc_eligible_atis in the past.
Named plugins: Not stored in oagw_plugin. Resolved via in-process registry. Not subject to GC.
Auth configuration references secrets via secret_ref (e.g., cred://partner-openai-key). OAGW does not manage secret sharing — this is handled by cred_store.
Resolution flow:
- OAGW resolves
secret_refviacred_storeAPI cred_storechecks if secret is accessible to current tenant (own or shared by ancestor)- If accessible → return secret material
- If not → return error, OAGW returns 401 Unauthorized
Ancestor can share a secret with descendants via cred_store policies. Descendant can also use own secret with different secret_ref.
Descendant's ability to override configurations depends on permissions granted by ancestors:
| Permission | Allows Descendant To |
|---|---|
oagw:upstream:bind |
Create binding to ancestor's upstream |
oagw:upstream:override_auth |
Override auth config (if sharing: inherit) |
oagw:upstream:override_rate |
Specify own rate limits (subject to min()) |
oagw:upstream:add_plugins |
Append own plugins to inherited chain |
Without appropriate permissions, descendant must use ancestor's configuration as-is (even with sharing: inherit).
Server-Side Request Forgery (SSRF):
- DNS: IP pinning rules, allowed segments matching.
- Headers: Well-known headers stripping and validation.
- Request Validation: Path, query parameters validation against route configuration.
Cross-Origin Resource Sharing (CORS): Built-in, configured per upstream/route. Preflight OPTIONS requests return a permissive 204 at the handler level (no upstream resolution or tenant context required). Origin validation happens on actual requests after upstream resolution, before forwarding. See ADR: CORS.
HTTP Version Negotiation: OAGW uses adaptive per-host HTTP version detection:
- First request: Attempt HTTP/2 via ALPN during TLS handshake
- Success: Cache "HTTP/2 supported" for this host/IP
- Failure: Fallback to HTTP/1.1, cache "HTTP/1.1 only" for this host/IP
- Subsequent requests: Use cached protocol version
Cache entry TTL: 1 hour. HTTP/3 (QUIC) support is future work.
Inbound Authentication & Authorization: All OAGW API requests require Bearer token authentication.
Management API permissions:
| Permission Required | Description |
|---|---|
gts.cf.core.oagw.upstream.v1~:{create;override;read;delete} |
Create/Override, read, delete upstream |
gts.cf.core.oagw.route.v1~:{create;override;read;delete} |
Create/Override, read, delete route |
gts.cf.core.oagw.auth_plugin.v1~:{create;read;delete} |
Create, read, delete auth plugin |
gts.cf.core.oagw.guard_plugin.v1~:{create;read;delete} |
Create, read, delete guard plugin |
gts.cf.core.oagw.transform_plugin.v1~:{create;read;delete} |
Create, read, delete transform plugin |
Proxy API permissions:
| Permission Required | Description |
|---|---|
gts.cf.core.oagw.proxy.v1~:invoke |
Proxy requests to upstreams |
Authorization checks:
- Token must have
gts.cf.core.oagw.proxy.v1~:invokepermission - Upstream must be owned by token's tenant or shared by ancestor
- Route must match request method and path
Outbound Authentication (OAGW → Upstream): Handled by auth plugins. Token refresh/caching may occur as part of credential preparation, but OAGW does not re-issue failed upstream requests.
Retry Policy: OAGW does not retry failed requests. Clients responsible for retry logic. Auth plugins handle token refresh on 401, but do not retry the original request.
ID: cpt-cf-oagw-interface-api
| Method | Path | Description |
|---|---|---|
POST |
/api/oagw/v1/upstreams |
Create upstream |
GET |
/api/oagw/v1/upstreams |
List upstreams |
GET |
/api/oagw/v1/upstreams/{id} |
Get upstream by ID |
PUT |
/api/oagw/v1/upstreams/{id} |
Replace upstream |
DELETE |
/api/oagw/v1/upstreams/{id} |
Delete upstream |
POST |
/api/oagw/v1/routes |
Create route |
GET |
/api/oagw/v1/routes |
List routes |
GET |
/api/oagw/v1/routes/{id} |
Get route by ID |
PUT |
/api/oagw/v1/routes/{id} |
Replace route |
DELETE |
/api/oagw/v1/routes/{id} |
Delete route |
POST |
/api/oagw/v1/plugins |
Create plugin |
GET |
/api/oagw/v1/plugins |
List plugins |
GET |
/api/oagw/v1/plugins/{id} |
Get plugin by ID |
DELETE |
/api/oagw/v1/plugins/{id} |
Delete plugin |
GET |
/api/oagw/v1/plugins/{id}/source |
Get Starlark source |
IDs use anonymous GTS identifiers: gts.cf.core.oagw.{type}.v1~{uuid}. Plugins are immutable (no PUT). DELETE returns 409 PluginInUse when referenced.
POST (Create):
- Server-generated UUID for all resources.
- Upstream: Alias auto-derived from hostname endpoints; explicit alias required for IP-based. Unique per
(tenant_id, alias)— returns 409 on conflict. If alias matches an ancestor upstream, the operation is a "bind" requiringoagw:upstream:bindpermission and respecting sharing mode constraints (enforceblocks overrides,privateblocks visibility). - Route:
upstream_idmust belong to the calling tenant — ancestor upstreams are not directly addressable. Validates match rule uniqueness within the upstream (same path + priority + method → 409).
PUT (Replace):
- Full replacement — all fields are overwritten; omitted optional fields are cleared.
- Upstream: Alias is recomputed when hostname endpoints change; only IP-based upstreams allow explicit alias updates. Re-validates ancestor bind constraints if overrides, endpoints, or alias changed.
- Route:
upstream_idis immutable (not present in the update DTO). Re-validates match rule uniqueness.
Immutable fields: id, tenant_id on all resources. Route upstream_id is also immutable.
All CRUD operations are strictly scoped to the calling tenant. Ancestor resources are invisible (404) to descendants via the management API.
| Operation | Own resources | Ancestor resources |
|---|---|---|
| POST (create) | Yes | N/A (bind via alias match) |
| PUT (replace) | Yes | 404 |
| DELETE | Yes | 404 |
| GET / List | Yes | 404 |
| Proxy (data plane) | Yes | Inherited via tenant chain walk |
At proxy time, resolve_alias walks the tenant chain (descendant → root) to find the closest enabled upstream by alias, then searches the chain for matching routes. Descendant routes take priority. Ancestor routes are inherited but cannot be viewed or modified through the management API.
All list endpoints support OData query parameters: $filter, $select, $orderby, $top, $skip.
Upstream:
| Parameter | Type | Description |
|---|---|---|
$filter |
string | OData filter expression (e.g., alias eq 'api.openai.com') |
$select |
string | Fields to return (e.g., id,alias,server) |
$orderby |
string | Sort order (e.g., created_at desc) |
$top |
integer | Max results (default: 50, max: 100) |
$skip |
integer | Offset for pagination |
Route:
| Parameter | Type | Description |
|---|---|---|
$filter |
string | OData filter (e.g., upstream_id eq '{uuid}') |
$select |
string | Fields to return |
$orderby |
string | Sort order |
$top |
integer | Max results (default: 50, max: 100) |
$skip |
integer | Offset for pagination |
Plugin:
| Parameter | Type | Description |
|---|---|---|
$filter |
string | OData filter (e.g., type eq 'guard') |
$select |
string | Fields to return |
$top |
integer | Max results |
$skip |
integer | Offset for pagination |
{METHOD} /api/oagw/v1/proxy/{alias}[/{path_suffix}][?{query_parameters}]
Request classification uses upstream.protocol to determine match strategy:
- HTTP: method allowlist + longest path prefix match
- gRPC (planned/Phase 3):
(service, method)match from gRPC request path (no gRPC proxy code path is currently implemented or reachable)
All gateway errors follow RFC 9457 Problem Details (application/problem+json) with GTS type identifiers.
| Error Type | HTTP | GTS Instance ID | Retriable | Description |
|---|---|---|---|---|
| RouteError | 400 | gts.cf.core.errors.err.v1~cf.oagw.validation.error.v1 |
No | General route validation error |
| ValidationError | 400 | gts.cf.core.errors.err.v1~cf.oagw.validation.error.v1 |
No | Request validation failed |
| MissingTargetHost | 400 | gts.cf.core.errors.err.v1~cf.oagw.routing.missing_target_host.v1 |
No | X-OAGW-Target-Host header required for multi-endpoint upstream with common suffix alias |
| InvalidTargetHost | 400 | gts.cf.core.errors.err.v1~cf.oagw.routing.invalid_target_host.v1 |
No | X-OAGW-Target-Host header format is invalid (must be hostname or IP, no port/path/special chars) |
| UnknownTargetHost | 400 | gts.cf.core.errors.err.v1~cf.oagw.routing.unknown_target_host.v1 |
No | X-OAGW-Target-Host value does not match any configured endpoint |
| AuthenticationFailed | 401 | gts.cf.core.errors.err.v1~cf.oagw.auth.failed.v1 |
No | Authentication to upstream failed |
| RouteNotFound | 404 | gts.cf.core.errors.err.v1~cf.oagw.route.not_found.v1 |
No | No matching route found |
| PluginInUse | 409 | gts.cf.core.errors.err.v1~cf.oagw.plugin.in_use.v1 |
No | Plugin in use |
| PayloadTooLarge | 413 | gts.cf.core.errors.err.v1~cf.oagw.payload.too_large.v1 |
No | Request payload exceeds limit |
| RateLimitExceeded | 429 | gts.cf.core.errors.err.v1~cf.oagw.rate_limit.exceeded.v1 |
Yes | Rate limit exceeded |
| SecretNotFound | 500 | gts.cf.core.errors.err.v1~cf.oagw.secret.not_found.v1 |
No | Referenced secret not found |
| ProtocolError | 502 | gts.cf.core.errors.err.v1~cf.oagw.protocol.error.v1 |
No | Protocol-level error |
| DownstreamError | 502 | gts.cf.core.errors.err.v1~cf.oagw.downstream.error.v1 |
Depends | Upstream service error |
| StreamAborted | 502 | gts.cf.core.errors.err.v1~cf.oagw.stream.aborted.v1 |
No | Stream connection aborted |
| LinkUnavailable | 503 | gts.cf.core.errors.err.v1~cf.oagw.link.unavailable.v1 |
Yes | Upstream link unavailable |
| CircuitBreakerOpen | 503 | gts.cf.core.errors.err.v1~cf.oagw.circuit_breaker.open.v1 |
Yes | Circuit breaker open |
| PluginNotFound | 503 | gts.cf.core.errors.err.v1~cf.oagw.plugin.not_found.v1 |
No | Plugin not found |
| ConnectionTimeout | 504 | gts.cf.core.errors.err.v1~cf.oagw.timeout.connection.v1 |
Yes | Connection timeout |
| RequestTimeout | 504 | gts.cf.core.errors.err.v1~cf.oagw.timeout.request.v1 |
Yes | Request timeout |
| IdleTimeout | 504 | gts.cf.core.errors.err.v1~cf.oagw.timeout.idle.v1 |
Yes | Idle timeout |
Standard Fields (RFC 9457):
type: GTS identifier for the error type (used for programmatic error handling)title: Human-readable summarystatus: HTTP status codedetail: Human-readable explanation specific to this occurrenceinstance: URI reference identifying the specific occurrence
Extension Fields (OAGW-specific):
upstream_id,host,path: Request contextretry_after_seconds: Retry guidancetrace_id: For distributed tracing correlation
OAGW distinguishes between gateway errors (originated by OAGW) and upstream errors (passthrough from upstream service) using the X-OAGW-Error-Source header. See ADR: Error Source Distinction.
- Gateway error:
X-OAGW-Error-Source: gateway— response body isapplication/problem+json - Upstream error:
X-OAGW-Error-Source: upstream— response body is passthrough from upstream as-is
Header may be stripped by intermediaries. For critical error handling, clients should combine header check with error response structure inspection.
Inbound (Client → OAGW): Bearer token authentication via modkit-auth.
| Permission | Description |
|---|---|
gts.cf.core.oagw.upstream.v1~:{create;override;read;delete} |
Upstream CRUD |
gts.cf.core.oagw.route.v1~:{create;override;read;delete} |
Route CRUD |
gts.cf.core.oagw.proxy.v1~:invoke |
Proxy requests |
Outbound (OAGW → Upstream): Handled by auth plugins. Credentials resolved from cred_store via secret_ref.
ID: cpt-cf-oagw-design-dependencies
| Dependency | Type | Purpose |
|---|---|---|
types_registry |
Internal | GTS schema/instance registration |
cred_store |
Internal | Secret material retrieval by URI reference |
api_ingress |
Internal | REST API hosting |
modkit-db |
Internal | Database persistence (SeaORM, multi-backend) |
modkit-auth |
Internal | Authentication & authorization |
pingora |
External | Reverse-proxy engine (connection pooling, load balancing) |
ID: cpt-cf-oagw-seq-proxy-flow
sequenceDiagram
participant C as Client
participant API as API Handler
participant DP as Data Plane
participant CP as Control Plane
participant Auth as Auth Plugin
participant Chain as Plugin Chain
participant US as Upstream Service
C->>API: POST /proxy/{alias}/{path}
API->>API: Extract SecurityContext
API->>DP: execute_proxy(alias, path, req)
DP->>CP: resolve_upstream(alias, tenant_id)
CP-->>DP: UpstreamConfig
DP->>CP: resolve_route(upstream_id, method, path)
CP-->>DP: RouteConfig
DP->>Auth: inject_credentials(request)
Auth-->>DP: authenticated request
DP->>Chain: execute_guards(request)
Chain-->>DP: validated request
DP->>Chain: transform_request(request)
Chain-->>DP: transformed request
DP->>US: HTTP request
US-->>DP: HTTP response
DP->>Chain: transform_response(response)
Chain-->>DP: transformed response
DP-->>API: ProxyResponse
API-->>C: HTTP response + X-OAGW-Error-Source
Client → API Handler (auth, validate DTO) → ControlPlaneService (validate, write DB) → Response
ID: cpt-cf-oagw-db-schema
Schema follows portable relational baseline with JSON blobs for evolving configuration. See ADR: Storage Schema for full details.
| Table | Purpose | Key Constraints |
|---|---|---|
oagw_upstream |
Tenant-scoped root config | PK: id, UNIQUE: (tenant_id, alias) |
oagw_route |
Route definitions | PK: id, FK: upstream_id (cascade) |
oagw_route_http_match |
HTTP match keys | PK: route_id, FK: cascade |
oagw_route_grpc_match |
gRPC match keys | PK: route_id, FK: cascade |
oagw_route_method |
HTTP method allowlists | PK: (route_id, method), FK: cascade |
oagw_upstream_tag / oagw_route_tag |
Discovery tags | PK: (parent_id, tag), FK: cascade |
oagw_plugin |
Custom plugins (UUID-backed) | PK: id, UNIQUE: (tenant_id, name) |
oagw_upstream_plugin / oagw_route_plugin |
Ordered plugin bindings | PK: (parent_id, position), FK: cascade |
- All reads/writes tenant-scoped through secure ORM (no raw SQL)
- Multi-table updates are atomic (single transaction)
- Route match determinism: no two enabled routes under same upstream may share
(path_prefix, priority)for same method - Plugin binding positions contiguous from 0, validated on write
- Named plugins referenced via
plugin_refwithplugin_uuid = NULL; custom plugins have both
All resources use anonymous GTS identifiers in API path parameters:
- Upstreams:
gts.cf.core.oagw.upstream.v1~{uuid} - Routes:
gts.cf.core.oagw.route.v1~{uuid} - Plugins:
gts.cf.core.oagw.{type}_plugin.v1~{uuid}
See ADR: Resource Identification for the layered resource model.
| Query | Description |
|---|---|
| Find Upstream by Alias | Lookup by (tenant_id, alias) with tenant hierarchy walk and enabled inheritance |
| List Upstreams for Tenant | List with shadowing (closest tenant wins) and enabled inheritance |
| Find Matching Route for Request | Match by (upstream_id, method, longest path prefix, priority) for HTTP; (upstream_id, service, method) for gRPC (planned/Phase 3 — no gRPC proxy code path is currently implemented or reachable) |
| Resolve Effective Configuration | Walk hierarchy, collect bindings, merge from root to child per sharing modes |
| List Routes by Upstream | Filter by upstream_id with tenant scoping |
| Track Plugin Usage | Scan oagw_upstream_plugin, oagw_route_plugin, and auth_plugin_uuid columns for references |
| Delete Garbage-Collected Plugins | Delete plugin rows where gc_eligible_at is in the past |
OAGW does not cache upstream responses. Caching is client/upstream responsibility.
Config caching (in-memory caching of effective upstream/route configuration to avoid DB reads on every proxy request) is a future consideration. See ADR: Control Plane Caching for design direction.
Prometheus metrics at /metrics (admin-only):
oagw_requests_total{host, path, method, status_class}— counteroagw_request_duration_seconds{host, path, phase}— histogramoagw_requests_in_flight{host}— gaugeoagw_errors_total{host, path, error_type}— counteroagw_circuit_breaker_state{host}— gaugeoagw_rate_limit_exceeded_total{host, path}— counter
Circuit Breaker Metrics:
oagw_circuit_breaker_transitions_total{host, from_state, to_state}— counter
Rate Limit Metrics:
oagw_rate_limit_usage_ratio{host, path}— gauge (0.0 to 1.0)
Routing Metrics:
oagw_routing_target_host_used{upstream_id, endpoint_host}— counter (tracks X-OAGW-Target-Host usage)oagw_routing_endpoint_selected{upstream_id, endpoint_host, selection_method}— counter (selection_method:explicit_header,round_robin,default)
Upstream Health Metrics:
oagw_upstream_available{host, endpoint}— gauge (0=down, 1=up)oagw_upstream_connections{host, state}— gauge (state:idle,active,max)
Cardinality management: no tenant labels, normalized paths from route config, status class grouping (2xx/3xx/4xx/5xx).
Histogram Buckets (request duration, seconds): [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
Structured JSON logs to stdout, ingested by centralized logging system (e.g., ELK, Loki). Fields: timestamp, level, event, request_id, tenant_id, principal_id, host, path, method, status, duration_ms, request_size, response_size, error_type.
No PII: Never log request/response bodies, query parameters, or headers (except allowlisted). No secrets: Never log API keys, tokens, or credentials. High-frequency sampling: rate-limited to prevent excessive log volume (e.g., sample 1/100 for high-volume routes).
What is Logged:
- Success requests: Request ID, tenant, host, path, method, status, duration, sizes
- Failed requests: All above + error_type, error_message
- Config changes: Upstream/route create/update/delete operations
- Auth failures: Failed authentication attempts (rate limited to prevent log flooding)
- Circuit breaker events: State transitions (CLOSED→OPEN, OPEN→HALF_OPEN, etc.)
Log Levels:
INFO: Successful requests, normal operationsWARN: Rate limit exceeded, circuit breaker open, retry guidance emitted (Retry-After)ERROR: Upstream failures, timeouts, auth failuresDEBUG: Detailed plugin execution (disabled in production)
- SSRF: IP pinning rules, allowed segments, scheme allowlist (HTTPS-only for MVP)
- CORS: Built-in handler per upstream/route, secure defaults (disabled unless configured)
- HTTP smuggling: Strict header parsing, reject CR/LF, validate CL/TE combinations
- HTTP/2: Adaptive per-host version detection with cached capability (1h TTL)
- DNS resolution / IP pinning rules (separate concern)
- Plugin versioning and lifecycle management (separate concern)
- Response caching (client/upstream responsibility)
- Automatic retries (client responsibility)
- HTTP/3 (QUIC) support (future)
- Database schema, indexing and queries
- ADR: Rust ABI / Client Libraries — HTTP client abstractions, streaming support, plugin development APIs
- [Core] Circuit breaker: config and fallback strategies — ADR: Circuit Breaker
- [Core] Concurrency control — ADR: Concurrency Control
- [Core] Backpressure queueing — ADR: Backpressure — In-flight limits, queueing strategies, graceful degradation under load
- [Plugin] Starlark standard library extensions (e.g., HTTP client, caching), with security considerations. Auth plugins may need network I/O.
- [Security] TLS certificate pinning — Pin specific certificates/public keys for critical upstreams to prevent MITM attacks
- [Security] mTLS support — Mutual TLS for client certificate authentication with upstream services
- [Protocol] gRPC support — HTTP/2 multiplexing with content-type detection — ADR: gRPC Support — Requires prototype
- [Deployment] Registry-only mode — All upstreams, routes, and plugin configs sourced exclusively from type registry (no management API CRUD). The
post_init()provisioning path already materializes registry entities through the full domain validation pipeline. A registry-only mode would require: (a) config flag to disable or make CRUD endpoints read-only, (b) soft-fail on invalid entities (skip with warning instead of blocking startup), (c) a validation feedback mechanism so config authors can discover rejected entities — e.g., status writeback on GTS entities or a dedicated provisioning status endpoint. This is a platform-level concern: any module consuming GTS entities for configuration faces the same write-time validation gap.
| PRD Requirement | Design Element |
|---|---|
cpt-cf-oagw-fr-upstream-mgmt |
cpt-cf-oagw-component-model — Control Plane handles upstream CRUD |
cpt-cf-oagw-fr-route-mgmt |
cpt-cf-oagw-component-model — Control Plane handles route CRUD |
cpt-cf-oagw-fr-request-proxy |
cpt-cf-oagw-seq-proxy-flow — Data Plane proxy request flow |
cpt-cf-oagw-fr-plugin-system |
cpt-cf-oagw-component-model — Three plugin types with trait-based extensibility |
cpt-cf-oagw-fr-rate-limiting |
cpt-cf-oagw-component-model — Token bucket with dual-rate config |
cpt-cf-oagw-fr-hierarchical-config |
cpt-cf-oagw-component-model — Sharing modes and merge strategies |
cpt-cf-oagw-fr-alias-resolution |
cpt-cf-oagw-component-model — Alias resolution with shadowing |
cpt-cf-oagw-fr-streaming |
cpt-cf-oagw-interface-api — SSE, WebSocket, gRPC streaming |
cpt-cf-oagw-nfr-low-latency |
cpt-cf-oagw-tech-dependencies — In-memory rate limiters |
cpt-cf-oagw-nfr-multi-tenancy |
cpt-cf-oagw-db-schema — All tables tenant-scoped via secure ORM |
cpt-cf-oagw-nfr-observability |
cpt-cf-oagw-interface-api — Prometheus metrics, structured logging |
cpt-cf-oagw-nfr-credential-isolation |
cpt-cf-oagw-principle-cred-isolation — cred_store secret references |
| ADR | Design Element |
|---|---|
| 0001 Component Architecture | cpt-cf-oagw-component-model |
| 0002 Request Routing | cpt-cf-oagw-component-model, cpt-cf-oagw-seq-proxy-flow |
| 0003 Plugin System | cpt-cf-oagw-component-model |
| 0004 Rate Limiting | cpt-cf-oagw-component-model |
| 0005 Circuit Breaker | cpt-cf-oagw-interface-api |
| 0006 CORS | cpt-cf-oagw-component-model |
| 0007 Control Plane Caching | cpt-cf-oagw-tech-dependencies |
| 0008 State Management | cpt-cf-oagw-component-model |
| 0009 Storage Schema | cpt-cf-oagw-db-schema |
| 0010 Resource Identification | cpt-cf-oagw-design-domain-model |
| 0011 Concurrency Control | cpt-cf-oagw-component-model |
| 0012 Backpressure and Queueing | cpt-cf-oagw-component-model |
| 0013 Error Source Distinction | cpt-cf-oagw-interface-api |
| 0014 gRPC Support | cpt-cf-oagw-interface-api |
| 0015 Rust ABI Client Library | cpt-cf-oagw-tech-dependencies |