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
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
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.
| # | 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. |
- API / runtime: Python 3.11+, FastAPI, Uvicorn, Pydantic v2 / pydantic-settings.
- Policy: Open Policy Agent (Rego) by default; Cedar (
cedarpy/cedarCLI) as an alternative — both behind onePolicyEngineinterface. - 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).
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. |
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
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.
make run # uvicorn on http://localhost:8080
# or the full stack (gateway + OPA + Postgres):
docker compose up --build| 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. |
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).
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.toolThe body
actoris required by the request model and is validated, but it is not trusted for authorization — the role comes from theX-API-Key.
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
ApprovalStoreis fine for the MVP/demo; for production, back approvals with a durable store (Postgres/DynamoDB) so pending approvals survive restarts. Seecerberus/pipeline/engine.py.
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.
- Authentication on by default. Identity + role come from the
X-API-Keyheader, resolved server-side; the request body'sactor.roleis 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.mdfor the threat model and the review findings that were fixed.
docs/ARCHITECTURE.md— components, data flow, and the rendered Mermaid diagrams.docs/POLICY.md— Rego + Cedar policy model.docs/AUDIT.md— the hash-chain design & verification.docs/DEMO.md— the four demo scenarios.docs/SECURITY.md— threat model + fixed review findings.CONTRIBUTING.md— dev setup, tests, and code style.CHANGELOG.md— release history.
See CONTRIBUTING.md for dev setup, running the tests, and the
ruff code style.
Created and maintained by cyberaidev.
Apache-2.0 — Copyright 2026 cyberaidev. See LICENSE.