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/verifyrecomputes the chain. - The broker keeps upstream credentials away from the agent (commercial). The broker injects a server's secret as
Authorization: Beareron 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:readorcatalog:writeon the virtual serveraegis-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 holdingcatalog:write. - Some actions belong to the operator alone. aegis accepts three actions only from the
defaulttenant: 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#
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.
- Configure a verifier. Set
AUTH_JWKS_URL,AUTH_ISSUERandAUTH_AUDIENCE. When no verifier is configured, every caller runs asanonymousin thedefaulttenant. - Set
DATABASE_URL. Without it, RBAC is off, every tool call is allowed, and the admin API returns 503. - Keep only the verifiers you trust. Every configured verifier is accepted on its own, so don't leave
AUTH_HS256_SECRETset alongside your IdP. Never setOAUTH_DEV_AUTHORIZE=1in production: it lets/oauth/authorizeissue a token for any subject with no login. - Turn on the audit log with a real key. Without
CLICKHOUSE_URLthere is no audit log. SetAUDIT_HMAC_KEYto a strong secret and deliver it as a Kubernetes Secret, through the chart'ssecrets.auditHmacKeyorsecrets.existingSecret. If it's unset, aegis falls back to a built-in development key. - Give every customer an explicit tenant claim. A token without one maps to
default, which is the operator tenant. - Keep logging at
info. The gateway fixes its log level atinfoin code and does not readRUST_LOG. If you change that in your own build, keep production atinfo, because HTTP client output atdebugandtracecan include header values. - 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.
- Set
METRICS_TOKENrather thanMETRICS_PUBLIC=1. Every route shares port 8080, so a network rule that admits Prometheus also admits/admin/v1. - Verify that your CNI enforces NetworkPolicy. The chart's NetworkPolicy is off by default. Once you enable it with
networkPolicy.enabled, runhelm testagainst 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
paramsonly, 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.
/healthzreturns 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.