Skip to content

Repository files navigation

Cerberus 🐺 — Secure Agent Gateway

A policy-enforcement gateway that guards every tool call an AI agent makes — allow, require human approval, or deny — with a tamper-evident audit trail.

Codename morpheus

CI security review license python

Cerberus is a policy-enforcement gateway that sits between an AI agent and the tools/APIs it wants to call. Every tool call is evaluated through a deterministic decision pipeline before it is allowed to execute. Denied calls never run; sensitive or risky calls require a human approval; and every decision is written to a tamper-evident, hash-chained audit log.

Like the three-headed hound of myth, Cerberus is the gate you cannot slip past; under the codename morpheus it shapes each agent request into an allow, an approval, or a hard stop.

Agent ──▶ Cerberus ──▶ (ALLOW) ──▶ Tool / AWS API
                │
                ├─▶ (DENY)             ✋ blocked, audited
                └─▶ (REQUIRE_APPROVAL) 🧑 human approves, then executes

Why

Autonomous agents are powerful and dangerous with production credentials. A model can be prompt-injected into deleting infrastructure or exfiltrating data. Cerberus gives you a single, auditable chokepoint that enforces least privilege, risk-aware approvals, and non-repudiable logging — independent of the model's own alignment.

Features — the six capabilities

# Capability How Cerberus delivers it
1 Tool allow-listing + schema validation S1 schema_validation rejects unknown tools and validates arguments against per-tool JSON Schemas (cerberus/tools).
2 Least-privilege authorization Server-side identity from X-API-Key, a local role→action check (S2 permission), and an external policy engine (S3 policy, OPA default / Cedar alt).
3 Risk detection S4 risk_detection runs pluggable rules (destructive ops, secrets access, PII export, oversized exports); CRITICAL→DENY, MEDIUM/HIGH→REQUIRE_APPROVAL.
4 Human-in-the-loop approvals S5 approval returns 202 + approval_id; an approver resolves it (separation of duties, no self-approval) and the agent re-executes via /v1/approvals/{id}/execute.
5 Tamper-evident audit Every decision (allow/deny/approval/dry-run) is appended to a hash-chained, append-only log with an external head anchor that also detects tail truncation.
6 Observability + graceful degradation Structured logging, opt-in OpenTelemetry spans, and fallbacks (local policy engine / in-memory store, Cedar fail-closed) so enforcement never silently disappears.

Stack

  • API / runtime: Python 3.11+, FastAPI, Uvicorn, Pydantic v2 / pydantic-settings.
  • Policy: Open Policy Agent (Rego) by default; Cedar (cedarpy / cedar CLI) as an alternative — both behind one PolicyEngine interface.
  • Audit store: Postgres (SQLAlchemy async + asyncpg) or DynamoDB (aioboto3); in-memory fallback for demo/tests.
  • Agent: LangGraph + langchain-core cloud-ops assistant; litellm for the (optional) LLM planner.
  • Observability: structlog + OpenTelemetry (SDK + OTLP exporter + FastAPI instrumentation).
  • Tooling / infra: Docker + docker compose, Terraform (infra/), ruff, pytest (+ pytest-asyncio, respx, moto).

Decision pipeline (most-restrictive wins)

Ordering: DENY > REQUIRE_APPROVAL > ALLOW

# Stage What it does
S1 schema_validation Reject unknown tools (allow-list) + validate args against JSON Schema. Hard halt on failure.
S2 permission Local role→action pre-check (defense in depth) using roles.json.
S3 policy Delegate to the policy engine (OPA by default, Cedar alt).
S4 risk_detection Run risk rules; CRITICAL→DENY, MEDIUM/HIGH→REQUIRE_APPROVAL.
S5 approval Require human approval for sensitive tools / risky calls without a valid token.
S6 finalize Reduce all stage verdicts into one PolicyDecision + PipelineTrace.

Architecture

Full request flow — agent → auth → POST /v1/execute → six-stage decision pipeline → ALLOW / REQUIRE_APPROVAL / DENY → hash-chained audit. The rendered diagram and a data:export approval sequence diagram live in docs/ARCHITECTURE.md (sources: docs/architecture.mmd, docs/data-flow.mmd).

flowchart LR
    agent["AI agent<br/>(LangGraph)"] -->|"tool call + X-API-Key"| auth{{"Auth (X-API-Key)<br/>server-side role"}}
    auth --> api["POST /v1/execute"]
    api --> pipe["DecisionEngine pipeline<br/>S1 schema → S2 permission → S3 policy<br/>→ S4 risk → S5 approval → S6 finalize"]
    pipe --> opa["OPA (default) / Cedar (alt)"]
    pipe --> out{"Decision"}
    out -->|ALLOW| exec["ToolExecutor (mock AWS)"]
    out -->|REQUIRE_APPROVAL| appr["ApprovalRequest → 202"]
    out -->|DENY| deny["blocked"]
    exec --> audit[("AuditStore<br/>Postgres / DynamoDB / Memory<br/>hash-chained + head anchor")]
    appr --> audit
    deny --> audit
Loading

Quick start (offline, no Docker)

make install          # create .venv and install the package + dev deps
make demo             # run the end-to-end offline demo
make test             # run unit + e2e tests (no OPA/Postgres/Docker needed)

The demo prints an ALLOW / DENY / DENY(critical) / approval→execute story and finishes with AUDIT VERIFY: valid=True.

Run the API

make run              # uvicorn on http://localhost:8080
# or the full stack (gateway + OPA + Postgres):
docker compose up --build

Key endpoints

Method Path Purpose
POST /v1/evaluate Evaluate a call (no side effects).
POST /v1/dry-run Evaluate + simulate execution (no side effects).
POST /v1/execute ALLOW→dispatch, REQUIRE_APPROVAL→202+approval_id, DENY→200.
GET /v1/approvals List approval requests.
POST /v1/approvals/{id}/approve | /deny Resolve an approval.
POST /v1/approvals/{id}/execute Resume an approved call.
GET /v1/audit | /v1/audit/{seq} Read the audit log.
GET /v1/audit/verify Verify the hash chain.
GET /v1/admin/tools Tool catalog.
POST /v1/admin/policies/reload Reload policies.
GET /healthz | /readyz Liveness / readiness.

Authentication

Auth is on by default. Identity and role are resolved server-side from an API key (X-API-Key header) — the actor.role in the request body is not trusted for authorization, so a caller cannot escalate by claiming role: admin. Requests without a valid key get 401. Approvals additionally require an approver role (default admin) and reject self-approval (403); the recorded approver is the authenticated principal, never a body-supplied id.

Dev default keys (used when CERBERUS_API_KEYS is unset): dev-readonly-key, dev-operator-key, dev-admin-key. For the offline demo/tests set CERBERUS_AUTH_DISABLED=true (then the actor comes from the body).

Example

curl -s localhost:8080/v1/execute \
  -H 'content-type: application/json' -H 'X-API-Key: dev-readonly-key' -d '{
  "actor": {"actor_id": "svc-readonly-agent", "role": "readonly"},
  "call": {"tool": "ec2:list_instances", "arguments": {"region": "us-east-1"}}
}' | python3 -m json.tool

The body actor is required by the request model and is validated, but it is not trusted for authorization — the role comes from the X-API-Key.

Configuration

Copy .env.example to .env. Everything is overridable via CERBERUS_* env vars — authentication (auth_disabled, api_keys, approver_roles), policy engine (opa/cedar), audit store (memory/postgres/dynamodb), risk thresholds, and telemetry. The gateway degrades gracefully: if OPA or the database is unreachable it falls back to a built-in local engine / in-memory store so it keeps enforcing policy.

CERBERUS_DESTINATION_ALLOWLIST (the exfiltration-check allow-list) must be a JSON array, e.g. [".internal","localhost"] — a comma-separated value fails to parse at startup. CERBERUS_AUTH_DISABLED=true and the dev default API keys are only permitted when CERBERUS_ENV is local or test; any other env fails fast at startup unless real CERBERUS_API_KEYS are configured.

The in-memory ApprovalStore is fine for the MVP/demo; for production, back approvals with a durable store (Postgres/DynamoDB) so pending approvals survive restarts. See cerberus/pipeline/engine.py.

Demo walkthrough

make demo (or python -m agent.run_demo) runs the LangGraph cloud-ops assistant against Cerberus, fully offline, through four scenarios:

# Goal Actor Verdict Why
1 List EC2 instances readonly ALLOW permitted read; result returned + audited
2 Terminate instances readonly DENY forbidden + destructive; never executes
3 Read a DB secret operator DENY forbidden and CRITICAL risk (secrets access)
4 Export customer data operator REQUIRE_APPROVAL → approve → ALLOW sensitive + PII; human approves, then executes

It then prints the audit records with linked hashes and calls verify, ending with AUDIT VERIFY: valid=True … 'chain intact'. Full expected output and the live-API version are in docs/DEMO.md.

Security notes

  • Authentication on by default. Identity + role come from the X-API-Key header, resolved server-side; the request body's actor.role is never trusted for authorization. No valid key → 401.
  • Separation of duties. Approvals require an approver role (default admin), self-approval is rejected (403), and the recorded approver is the authenticated principal — not a body-supplied id.
  • Tamper-evident audit. The append-only hash chain detects field tampering and reordering; an external head anchor (head_seq, head_hash) additionally catches tail truncation (dropping the newest records), which a plain chain cannot detect on its own.
  • Fail-closed. OPA/DB outages fall back to the local engine / in-memory store; the Cedar engine's static fallback denies anything outside a role's permissions.
  • Security review score: 96/100. See docs/SECURITY.md for the threat model and the review findings that were fixed.

Documentation

Contributing

See CONTRIBUTING.md for dev setup, running the tests, and the ruff code style.

Author

Created and maintained by cyberaidev.

License

Apache-2.0 — Copyright 2026 cyberaidev. See LICENSE.

About

Cerberus — a policy-enforcement gateway for AI agents: allow-list + JSON-schema validation, per-role permissions, human approval for sensitive actions, risky-argument detection, and a tamper-evident audit trail.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages