Skip to content

aegis · Concepts

How aegis works

The ordered request pipeline behind every aegis tool call, from authentication to audit, and the Redis, Postgres and ClickHouse stores behind it.

On this page

The request pipeline#

Every tools/call runs the same ordered sequence of checks. Cheap checks run first and the upstream forward runs last, so a refused call never reaches your MCP server. Any stage can refuse the call, and each refusal carries its own JSON-RPC code (see Error codes).

  1. Authenticate. Middleware on the protected routes verifies the bearer JWT: HS256 with a shared secret, RS256 through a JWKS endpoint, or a token from aegis's own OAuth issuer (commercial). A missing or invalid token gets HTTP 401 with a plain JSON body, not a JSON-RPC error. So does a valid token whose tenant is not listed in AUTH_ALLOWED_TENANTS, when that variable is set.
  2. Correlation ID. aegis adopts the trace ID from the caller's W3C traceparent header, or mints one. It is returned in X-Correlation-ID and sent upstream as a child traceparent.
  3. Parse. A body that is not JSON gets HTTP 400 with -32700. JSON that is not a valid JSON-RPC 2.0 request gets HTTP 400 with -32600. A notification (no id) gets HTTP 202 with an empty body and goes no further.
  4. Session. Without an X-Session-ID header, aegis mints a session. A session you supply must exist (otherwise HTTP 401) and belong to you (otherwise HTTP 403).
  5. Rate limit. Per session (RATE_LIMIT_PER_SESSION, default 120 a minute) and per session and tool (RATE_LIMIT_PER_TOOL, default 60 a minute). 0 turns a limit off. Over the limit gets HTTP 429 with -32029.
  6. Monthly quota (commercial). With billing enabled and BILLING_ENFORCE=1, a tenant over its monthly quota gets HTTP 402 with -32060. With billing enabled but not enforced, the overage is counted in metrics and the call continues.
  7. Route. aegis resolves the owning server once and uses that answer for both the RBAC check and the forward, so nothing can change between check and use. An unknown tool gets -32601 after a throttled rediscovery.
  8. Kill-switch. A switch on the tenant, server or tool refuses the call with -32062. It overrides RBAC.
  9. RBAC. With DATABASE_URL set, a call with no matching allow, or with any matching deny, gets HTTP 403 with -32003. A deny wins over approve, and approve wins over allow.
  10. Approval hold. When an approve policy matches, the call is held and refused with -32061 and an approval_id. After an operator approves it, the same caller can run the identical call (same arguments) once, within APPROVAL_TTL_SECS (default 3600) of the original request.
  11. Input-schema validation. When the tool declares an inputSchema, the arguments are checked against it. A violation gets -32602. INPUT_VALIDATION defaults to enforce. monitor logs the violation instead, and off skips the check.
  12. Threat gate. The whole params subtree is scanned. A quarantined tool gets -32050 and an injection pattern gets -32051. A credential, card number, SSN or private key in the outbound arguments gets -32053 (EGRESS_ACTION=monitor logs it instead, and off skips the scan). THREAT_MODE=monitor logs all of these without blocking.
  13. Credential broker (commercial). If the server declares a secret_ref, aegis resolves it from AWS Secrets Manager and injects it as Authorization: Bearer. If it cannot, or the broker is turned off, the call is refused with -32054.
  14. Forward. Backpressure and the circuit breaker, both keyed by tenant and server, fast-fail with -32063. The breaker opens after CIRCUIT_FAILURE_THRESHOLD (default 5) consecutive failures. Connection failures are retried up to UPSTREAM_MAX_RETRIES (default 2), and https upstreams can use mTLS. A timeout (UPSTREAM_TIMEOUT_SECS, default 10) gets -32064 with "safe_to_retry": false, because the tool may have run. Any other upstream failure gets -32000.
  15. Response scan. Any echo of a brokered credential becomes [REDACTED:UPSTREAM_CREDENTIAL]. The response is then scanned for PII and secrets. By default each hit is redacted as [REDACTED:KIND]. With PII_ACTION=block, a response carrying a credential, card number, SSN or private key is refused with -32052. PII_ACTION=monitor only logs.
  16. Record and return. The decision is recorded, the call is metered if billing is on, and the response is fanned out to SSE subscribers on the session before it returns with X-Session-ID and X-Correlation-ID.

From stage 7 on, every refusal except an RBAC deny comes back as HTTP 200 with a JSON-RPC error body. Check error.code, not only the HTTP status.

One recording path#

From stage 7 on, every outcome goes through one recording step. It increments the Prometheus decision counters and writes one audit event, which goes to the HMAC-SHA256 hash-chained audit log in ClickHouse and to the SIEM sinks (commercial). Integrity, injection, PII and credential blocks (-32050, -32051, -32052, -32054) also go to the alert channels (commercial). Admin changes to servers, roles, policies, role grants, approvals, kill-switches and quarantines go through the same recording step. Because one event feeds every destination, you do not wire the audit log, SIEM and alerts separately.

Rate-limit and quota refusals stop before stage 7. They are counted in Prometheus but not written to the audit log. See Audit and observability.

Background work#

  • Integrity re-sweep. Every INTEGRITY_RESWEEP_SECS (default 15, 0 disables), aegis re-lists each upstream's tools and compares every definition with its SHA-256 pin. A changed definition is logged and alerted (commercial). Unless THREAT_MODE=monitor, it is also quarantined: it disappears from tools/list and calls get -32050 until an operator re-pins it with POST /admin/v1/quarantine. Quarantine is sticky, so reverting the upstream does not lift it.
  • Shadow sweep. At startup, after catalog changes and every SHADOW_SWEEP_SECS (default 60), aegis compares tool names across servers for exact shadows and look-alike names. Findings appear at GET /admin/v1/shadows. The sweep only reports. It never blocks.
  • Catalog reconciler. With DATABASE_URL set, every CATALOG_RECONCILE_SECS (default 30) aegis compares the enabled servers in Postgres with its routing table and rebuilds the table if they differ.
  • Kill-switch cache. Every KILLSWITCH_REFRESH_SECS (default 2), each replica reloads the switch set from Redis. The replica that flips a switch applies it at once, and the others pick it up on their next refresh.
  • Policy snapshot. This refresh runs on demand, not on a timer. When the RBAC snapshot is older than POLICY_REFRESH_SECS (default 30), a single request reloads it while the others keep using the current snapshot. Policy changes take effect within about that window.

MCP surface#

Point MCP clients at POST /mcp. POST /v1/invoke is the same handler at its original path.

MethodWhat aegis does
initializeReturns protocolVersion (default 2025-06-18, set with MCP_PROTOCOL_VERSION), capabilities {"tools":{"listChanged":false}} and server name aegis-gateway
pingReturns an empty result
tools/listReturns your tenant's tools, filtered by RBAC, with quarantined tools left out
tools/callRuns the pipeline above
NotificationsHTTP 202 with an empty body
Any other method-32601

aegis does not advertise or proxy resources or prompts. It tracks sessions in X-Session-ID (1-hour TTL, refreshed on use) and does not issue an Mcp-Session-Id to clients. To see results live, open GET /v1/subscribe?session_id=... as an SSE stream, as the session's owner. Each response an upstream returns on that session goes out to every subscriber, up to 64 per session. aegis's own refusals and errors are not streamed, and a slow subscriber drops events instead of slowing the call.

Toward your servers, aegis is an MCP client over Streamable HTTP. It runs initialize once per upstream (and again if the upstream expires the session with HTTP 404), echoes any Mcp-Session-Id the upstream issues, and accepts a JSON or SSE reply. Each upstream request is built from scratch, so your client's headers are never passed through.

Backing services#

ServiceWhat aegis keeps thereIf it is missing
RedisSessions (1-hour TTL), rate-limit counters, kill-switches, OAuth authorization codes, the usage meterRequired. The gateway exits at startup if it cannot connect.
PostgresServer catalog, RBAC users, roles and policies, approvals, billing accountsOptional (DATABASE_URL). Without it, aegis proxies a single UPSTREAM_URL, RBAC is disabled and the admin API returns HTTP 503.
ClickHouseThe hash-chained audit_log tableOptional (CLICKHOUSE_URL). Without it, there is no audit log.
EnvoyTLS termination on :8443Local Compose stack only, with a self-signed certificate.

Shared state lives in these stores, so the gateway scales horizontally. The Helm chart runs 2 replicas by default (see Deploy aegis). Some state stays in each replica's memory:

  • SSE subscriptions. A subscriber only receives responses for calls that its own replica handles. If you rely on /v1/subscribe, keep each session on one replica.
  • Integrity pins, quarantines and shadow findings. Each replica builds these from its own sweeps, and a restarted replica pins every tool again on first sight. GET and POST /admin/v1/quarantine act on the replica that serves the request.
  • Circuit-breaker and backpressure state. UPSTREAM_MAX_CONCURRENT (default 64, 0 disables) caps in-flight calls to each upstream on each replica, so the cluster-wide ceiling grows with the replica count.

Failure posture#

ComponentWhen it failsPosture
Redis at startupThe gateway exits before it binds :8080Closed
Postgres at startup (DATABASE_URL set)The catalog load is retried up to 10 times, then the gateway exitsClosed
Session storeA lookup or create error returns HTTP 500Closed
Rate limiterA Redis error lets the call throughOpen
Billing meter and quotaAn error lets the call throughOpen
Kill-switchLast-known switches stay in effect, and an outage never adds oneLast known
Policy snapshotA failed reload keeps the previous snapshot enforced. If the first load fails, calls are denied until one succeeds.Last known
Approvals storeCalls that need approval are refused with -32061 and "status": "unavailable"Closed
Credential brokerCalls to a server with a secret_ref are refused with -32054Closed
Audit writerIf ClickHouse is unreachable at startup, the audit log is disabled and an error is logged. A full queue (4,096 events) or a failed insert drops the event and logs an error.Open
Invalid mTLS settingsThe gateway refuses to startClosed
AUTH_ALLOWED_TENANTS set but emptyEvery tenant is refusedClosed