Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agent-broker

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.

Try it in 60 seconds

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
./demo/run_demo.sh

macOS/Linux, Python 3.9+ (WSL on Windows).

What it does

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).

Threat model

  • 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.5 leaks 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.

Demo

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.

Prior art and what's different

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.

What this does NOT protect against

  • 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.

Where this goes

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.

Roadmap

  • 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).

Running tests

.venv/bin/python -m pytest

./check.sh runs the tests and then scans the repository for anything that looks like a real credential.

Running for real

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.

About

Deterministic credential and egress enforcement for AI agents in multi-tenant SOC/MSSP environments.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages