Skip to content

aegis · Concepts

Security model

What aegis defends against, what it trusts, where it fails closed, and the limits you should plan around when you run it in front of MCP servers.

On this page

This page covers what aegis trusts, where it fails closed, what to set before it takes real traffic, and what it does not cover.

Threat model#

aegis treats both ends of a tool call as untrusted: the agent that sends the call and the MCP server that answers it.

The agent's arguments. A tools/call can carry shell payloads, instructions aimed at a downstream model, or credentials on their way out. Before forwarding a call, aegis validates the arguments against the tool's inputSchema. It then scans every string in the call's params, object keys as well as values, against its injection rules: 35 run on every tool, and 8 more run only on tools you list in INJECTION_SHELL_TOOLS. Last, it checks the same strings for secrets heading out to the upstream.

The upstream's response. aegis assumes a compromised server controls the entire response JSON. It scans both result and the error message and data for PII and secrets: object values and keys, array elements and numeric leaves. The only thing it skips is a genuine base64 data: URI.

The upstream's tool definitions. aegis pins each definition by SHA-256, re-checks it in the background and compares tool names across servers. It does not compile the schema pattern keyword, so an upstream can't hand the gateway a regex built to stall it.

Tenants that register servers. Once a server is in the catalog, aegis dials its base_url on a schedule, so it screens catalog URLs before storing them. The SSRF guard section below covers how.

Where the trust boundary sits#

  • The audit key stays in the gateway. Each audit row carries an HMAC-SHA256 over the previous row's tag and the row's own content, keyed by AUDIT_HMAC_KEY. ClickHouse never sees that key, so someone with write access to the database can't produce a valid re-chained history after they change it. GET /admin/v1/audit/verify recomputes the chain.
  • The broker keeps upstream credentials away from the agent (commercial). The broker injects a server's secret as Authorization: Bearer on an upstream request that aegis builds fresh, without the caller's headers. If the upstream echoes that exact credential back, aegis replaces it with [REDACTED:UPSTREAM_CREDENTIAL] before the response goes to the caller or out over SSE.
  • SCIM is a separate machine channel (commercial). It uses its own bearer tokens, each bound to one tenant, and it writes only users, roles and role memberships. Policies stay admin-defined.
  • RBAC governs the admin API too. Admin routes need catalog:read or catalog:write on the virtual server aegis-admin. A lock-out guard rolls back any role, policy or membership change, from the admin API or from SCIM, that would leave the tenant with no subject holding catalog:write.
  • Some actions belong to the operator alone. aegis accepts three actions only from the default tenant: setting or lifting a kill-switch, verifying the audit chain, and setting a tenant's billing tier (commercial). A tenant can't lift a kill-switch placed on it.

Fail-closed defaults#

ControlDefaultBehaviour
THREAT_MODEenforceOnly monitor makes threat checks log-only.
EGRESS_ACTIONblockCredentials, card numbers, SSNs or private keys in a call's params refuse the call with -32053.
INPUT_VALIDATIONenforceSchema violations return -32602.
PII_ACTIONredactResponse hits are masked as [REDACTED:KIND].
ApprovalsclosedIf Postgres is unreachable, an approve-gated call doesn't run. Requesters can't approve their own calls.
Credential brokerclosedA secret_ref that can't be resolved returns -32054.
/metricsclosedReturns 503 until you set METRICS_TOKEN or METRICS_PUBLIC=1.
AUTH_ALLOWED_TENANTSunsetUnset serves every tenant. Set but empty, it refuses every tenant.
Kill-switchlast knownWhile Redis is unreachable, the switches already in force stay in force.

The /dashboard console is served with a Content-Security-Policy that sets default-src 'none', connect-src 'self' and frame-ancestors 'none', plus X-Frame-Options: DENY and Referrer-Policy: no-referrer. It renders API data as DOM text and never parses it as HTML.

SSRF guard on the catalog#

aegis refuses a base_url with HTTP 400 when the literal host is loopback, link-local (169.254.0.0/16 or fe80::/10), unspecified, multicast, broadcast, or an obfuscated form of any of these. RFC1918 addresses are allowed on purpose, because pod and service IPs use them.

The guard checks only the literal host. A hostname that resolves to a blocked address still passes, because the HTTP client resolves it later, at connect time. Pair the guard with an egress NetworkPolicy. If the credential broker gets AWS credentials from instance metadata or EKS Pod Identity, allow those endpoints in your egress rules, or brokered calls will fail closed.

Hardening guidance#

Set each of these before you expose the gateway. Variables are listed in Configuration, and chart values in Deployment.

  1. Configure a verifier. Set AUTH_JWKS_URL, AUTH_ISSUER and AUTH_AUDIENCE. When no verifier is configured, every caller runs as anonymous in the default tenant.
  2. Set DATABASE_URL. Without it, RBAC is off, every tool call is allowed, and the admin API returns 503.
  3. Keep only the verifiers you trust. Every configured verifier is accepted on its own, so don't leave AUTH_HS256_SECRET set alongside your IdP. Never set OAUTH_DEV_AUTHORIZE=1 in production: it lets /oauth/authorize issue a token for any subject with no login.
  4. Turn on the audit log with a real key. Without CLICKHOUSE_URL there is no audit log. Set AUDIT_HMAC_KEY to a strong secret and deliver it as a Kubernetes Secret, through the chart's secrets.auditHmacKey or secrets.existingSecret. If it's unset, aegis falls back to a built-in development key.
  5. Give every customer an explicit tenant claim. A token without one maps to default, which is the operator tenant.
  6. Keep logging at info. The gateway fixes its log level at info in code and does not read RUST_LOG. If you change that in your own build, keep production at info, because HTTP client output at debug and trace can include header values.
  7. Terminate TLS with a real certificate. The gateway itself serves plain HTTP on port 8080. The Compose stack's Envoy certificate is self-signed, so use it for development only.
  8. Set METRICS_TOKEN rather than METRICS_PUBLIC=1. Every route shares port 8080, so a network rule that admits Prometheus also admits /admin/v1.
  9. Verify that your CNI enforces NetworkPolicy. The chart's NetworkPolicy is off by default. Once you enable it with networkPolicy.enabled, run helm test against your release to check that ingress is actually denied, because some CNIs accept NetworkPolicy objects but never enforce them.

Known limits#

  • Integrity pinning is trust-on-first-use. If a tool is malicious the first time aegis sees it, that version becomes its own baseline.
  • Pins and quarantines live in process memory. Each gateway replica keeps its own set, and a restart starts trust-on-first-use again from each tool's current definition.
  • Quarantine is sticky. An ordinary upstream version bump also quarantines a tool. The tool stays quarantined until an operator releases it with POST /admin/v1/quarantine, which pins its current definition.
  • Shadow and typosquat detection only reports. It never blocks a call.
  • Audit truncation during downtime goes undetected. If rows are cut from the end of the log while the gateway is down, verification doesn't catch it after restart. Catching that needs external notarization of the chain head, which hasn't been built yet.
  • Audit writes never block a call. If the 4,096-event audit queue is full, new events are dropped and logged as errors.
  • Injection detection uses pattern rules. It is not a complete defence against prompt injection, and it scans a call's params only, not tool responses or tool descriptions.
  • Rate limiting fails open. Calls aren't rate-limited while Redis is unavailable.
  • Health checks don't cover the audit log. /healthz returns a static 200. If ClickHouse can't be reached during about 30 seconds of retries at startup, the audit log stays disabled until the next restart, so alert on that startup error.
  • Tools only. aegis doesn't proxy MCP resources or prompts.