Don't put secrets in every agent. Put enforcement in one place.
The enforcement layer is deterministic code — no model, no prompt. Agents reason; the broker only says allow or deny.
Multi-tenant egress control for AI agents in SOC/MSSP environments. Agents get permission, not credentials.
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
./demo/run_demo.sh
macOS/Linux, Python 3.9+ (WSL on Windows).
Agents never receive upstream API credentials; each holds only a scoped broker token. One broker holds the upstream keys and makes every outbound call itself. Credentials are encrypted at rest in one store and tenant-scoped at execution time; per-tenant keys are on the roadmap. Each request passes through, in order:
auth → tenant resolution → endpoint policy → argument validation → rate limit → credential injection → outbound call → audit
Every step is a named function in its own file under broker/, with a comment on why it exists. Upstreams today: VirusTotal (IPs, file hashes) and AbuseIPDB (IPs).
- Hijacked / prompt-injected agent. A valid caller starts doing things its author never intended. Every request is judged on its merits, not on who sent it.
- Confused deputy. A real identity is used to reach an endpoint it was never granted. Per-caller endpoint policy refuses it with 403.
- Quota / spend abuse. One runaway agent burns the shared upstream budget. Each caller has its own sliding-window limit; the excess gets 429 and never leaves the box.
- Internal-range exfiltration via an external API. Asking VirusTotal about
10.0.0.5leaks internal topology to a third party. Private, loopback, link-local, multicast and reserved ranges are refused with 400 before any outbound call. - Secret leakage through logs or responses. Keys are Fernet-encrypted at rest, decrypted only for one outbound call, and never placed in a response. Rejected input is audited only as a SHA-256 and length, never raw. Tokens are stored only as SHA-256 hashes.
- Cross-tenant credential access. Every caller is pinned to one tenant. Credential selection indexes by that tenant only. A tenant with no key gets
no_credential_for_tenant, never a neighbour's key or a default. - Stolen broker token. A stolen broker token yields that agent's identity, tenant, endpoints and budget — not the upstream API keys. Short-lived or workload-identity tokens are future work.
Run ./demo/run_demo.sh (see "Try it in 60 seconds" above for setup). No network or real keys needed. The script generates a throwaway master key and fake per-tenant upstream keys, starts the broker with MOCK_UPSTREAM=true, runs a well-behaved agent (triage-agent-01), then a hijacked one (triage-agent-02, same tenant, same permissions, separate token and budget), then queries both budgets and summarises audit.jsonl.
Output from a real run:
== hijacked_agent (triage-agent-02, tenant acme, injected instructions) ==
attempt expected actual result
------------------------------------ ------------------------ ------------------------ ------
500 rapid /enrich/ip requests 30 x 200, 470 x 429 30 x 200, 470 x 429 PASS
enrich internal IP 10.0.0.5 400 private_range 400 private_range PASS
enrich internal IP 192.168.1.1 400 private_range 400 private_range PASS
enrich internal IP 127.0.0.1 400 private_range 400 private_range PASS
malformed input: '; DROP TABLE' 400 invalid_format 400 invalid_format PASS
malformed input: empty string 400 invalid_format 400 invalid_format PASS
malformed input: 10 KB string 413 body_too_large 413 body_too_large PASS
/enrich/hash with report-agent token 403 endpoint_not_allowed 403 endpoint_not_allowed PASS
request with no token 401 unknown_token 401 unknown_token PASS
9/9 attacks contained
== remaining rate budget (GET /quota) ==
triage-agent-01 tenant=acme remaining 23/30
triage-agent-02 tenant=acme remaining 0/30
legit agent unaffected by the burst
== audit summary (audit.jsonl: 517 requests) ==
allowed: 39
denied: 478
by reason:
denied rate_limited 470
allowed ok 39
denied private_range 3
denied invalid_format 2
denied body_too_large 1
denied endpoint_not_allowed 1
denied unknown_token 1
by caller:
triage-agent-02 507
triage-agent-01 8
report-agent 1
<no identity> 1
by tenant:
acme 515
globex 1
<none> 1
enrichment actions authorized upstream:
ok 37
no API key or bearer token found in audit.jsonl
37 of 517 broker requests were authorized to reach the upstream integration layer; 478 were denied. Those 37 are the legit agent's 7 enrichment actions plus the hijacked agent's first 30. An IP enrichment fans out to two upstream HTTP calls (VirusTotal and AbuseIPDB) and a hash enrichment to one, so the 37 authorized actions produced 72 upstream requests in total. The two /quota reads are the difference between 39 allowed and 37 authorized.
Credential brokering for agents is an emerging architectural pattern: an individual Internet-Draft (not an IETF standard) describes a "Credential Broker for Agents," and several MCP gateways and cloud agent-identity services implement pieces of it. This project is SOC-specific: the endpoints, validators and threat model are built around threat-intel enrichment, not generic tool calls. It is vendor-neutral (plain HTTP, not MCP-only), multi-tenant by design, and local-first, with a single process and no cloud dependency. And it ships with an adversarial test suite and a hijacked-agent demo rather than a claim.
- The broker is a high-value target. Compromise of the host or the master key exposes every tenant's credentials it holds. Per-tenant KMS/HSM-backed keys and network isolation are out of scope for this version.
- Agent hijacking itself. It does not stop an agent from being hijacked; it bounds what a hijacked agent can do.
- Consequential actions. There is no approval gate, because the broker exposes only read-only enrichment. Adding a write-capable upstream without one would be a mistake.
- Multi-instance rate limits. Limits are per process, in memory. They do not survive a restart and do not aggregate across instances.
- Abuse within policy. A hijacked agent that stays under its limit and asks only about public indicators gets answers. The broker enforces what may be asked, not why.
Direction, not current state:
- Integrations implemented once in the broker and exposed as capabilities to authorized agents.
- Per-tool actions with allow / deny / require-approval.
- Per-tenant KMS-backed keys.
- Approval gate for write or otherwise consequential actions.
- Per-tenant KMS-backed secrets.
- More upstreams: GreyNoise, Shodan.
- Delegated-user attribution in the audit log (which human's task an agent was acting on).
.venv/bin/python -m pytest
./check.sh runs the tests and then scans the repository for anything that looks like a real credential.
export BROKER_MASTER_KEY="$(python -m broker.secrets generate-key)" # keep out of the repo
VT_API_KEY=... ABUSEIPDB_API_KEY=... python -m broker.secrets init --tenant acme # repeat per tenant
python -m broker.secrets new-token --name triage-agent-01 # paste the hash into policies.yaml
uvicorn --factory broker.app:create_app --host 127.0.0.1 --port 8787
Then POST /enrich/ip or POST /enrich/hash with {"value": "..."} and Authorization: Bearer <token>. GET /quota returns the caller's remaining budget.