agenthound loot --type litellm — LiteLLM Looter operator guide¶
Authorized engagements only. Looting a LiteLLM gateway with the master key extracts upstream provider credentials (OpenAI, Anthropic, AWS Bedrock, Azure, Cohere keys behind the master) AND leaves a clear audit trail in the gateway's Postgres backend, LangFuse instrumentation (if present), and any defender SIEM watching the proxy. Coordinate with the target's IR/security team out-of-band BEFORE running this against production. The first
agenthound lootinvocation on a machine triggers an interactiveAUTHORIZEDprompt and writes a sentinel file (~/.agenthound/loot-acknowledged) to record the acknowledgement.
The LiteLLM Looter is the v0.2 marquee action — the first concrete Looter against a high-leverage target. A single LiteLLM master key compromise yields aggregated provider credentials for every upstream the gateway proxies. This is what lights up the credential-chain finding in the AgentHound graph.
What the looter does¶
Three GET-only HTTP probes against a fingerprinted LiteLLM gateway:
-
GET /model/info(master-key authenticated) — lists every upstream provider model the gateway proxies, includinglitellm_params.api_baseand (when LiteLLM exposes it)litellm_params.api_key. Emits one:Credentialnode per provider withtype=apiKey,provider=openai|anthropic|aws_bedrock|...,value_hashpopulated. -
GET /key/list(master-key authenticated) — enumerates virtual keys with their spend and model allowlist. Emits one:Credentialnode per virtual key withtype=virtual_key. Failure here (common — many production deployments restrict/key/list) does not abort the loot; it lands inLootResult.PartialErrorsand the looter continues. -
The master-key Credential itself — emitted unconditionally as the FIRST node, with
value_hash = HashCredentialValue(master_key). This is the cross-collector merge primitive: if the same secret appears asLITELLM_MASTER_KEYin an MCP config, the Config Collector emits a:Credentialwith the samevalue_hash, and thecross_service_credential_chainpost-processor joins the two sides into a credential-chain finding.
All emissions land in a single EXPOSES_CREDENTIAL star: the :LiteLLMGateway:AIService node points at every emitted :Credential with edge evidence {endpoint, source, engagement_id}.
The looter is read-only by contract. A regression test (modules/litellmloot/get_only_test.go) asserts only GET and HEAD requests are issued. Any future probe that mutates state on the target — POST, PUT, DELETE — must be a Poisoner module, not an addition to this Looter.
Quick start¶
# First invocation triggers AUTHORIZED prompt + writes ~/.agenthound/loot-acknowledged.
agenthound loot 10.0.0.10:4000 --type litellm \
--master-key sk-ENGAGEMENT-MASTER-KEY \
--engagement-id RTV-DEMO-2026-001 \
--output -
# Subsequent invocations skip the prompt (sentinel exists).
agenthound loot 10.0.0.10:4000 --type litellm \
--master-key sk-... --engagement-id ENG-002 --output ./loot.json
# Stream straight into the analysis server.
agenthound loot 10.0.0.10:4000 --type litellm \
--master-key sk-... --engagement-id ENG-003 --output - | \
agenthound-server ingest -
# Generic --credential form (works for any future Looter without per-module flags).
agenthound loot 10.0.0.10:4000 --type litellm \
--credential master_key=sk-... --engagement-id ENG-004
Flags¶
| Flag | Required | Default | Notes |
|---|---|---|---|
--type litellm |
Yes | — | Module dispatcher key. Resolves via module.GetByTarget("litellm", action.Loot). |
--master-key sk-... |
One of | — | Sugar for --credential master_key=.... |
--credential KEY=VALUE |
One of | — | Generic per-module credential form, repeatable. The LiteLLM Looter reads master_key. |
--engagement-id <id> |
Recommended | empty | Recorded in every emitted edge's evidence map and on every slog line. Coordinate with target IR for attribution. |
--include-credential-values |
No | false |
When false (default), Credential nodes carry value_hash only — the raw value is omitted. When true, the raw value property is also populated. The hash is always populated. |
--max-items <n> |
No | 1000 | Caps emitted Credential nodes per category. |
--output <path> |
No | ./loot-<scan_id>.json |
Use - for stdout. |
--timeout <duration> |
No | 30s | Total probe timeout. |
Audit-trail residue caveat¶
Looting LiteLLM is not invisible. Every probe shows up in:
- LiteLLM's Postgres backend — every authenticated request is logged with the master key prefix, the requesting IP, the response status, and the response body length.
- Cloud HTTP access logs — if the gateway sits behind a load balancer (ALB, Cloud Run, Cloudflare), every probe is in the access log.
- LangFuse instrumentation — if the gateway emits LangFuse events for
/model/infoor/key/list, those events show up in the LangFuse trace store with the master key's session ID. - Defender SIEMs — if the operator's SOC ships LiteLLM logs to a SIEM, the loot session appears as a single-source burst of admin-API calls. Splunk/Datadog rules that alert on master-key-from-new-IP will fire.
The --engagement-id flag is the load-bearing operational mitigation. Communicate it to the target's IR team out-of-band BEFORE running. Then when the SOC sees the burst and pages someone, the engagement-id in the slog output (and on every emitted edge's evidence) is the correlation key that lets IR confirm "yes, this is the authorized assessment, not an incident." Without an engagement-id you have a non-zero chance of triggering a real incident response.
The looter does NOT scrub master keys from slog output — it redacts to an 8-character prefix (sk-ENGAG...). The full master key never appears in stdout, stderr, or the emitted JSON unless --include-credential-values is set. A regression test (modules/litellmloot/redaction_test.go) asserts the full master key never appears in slog across any code path.
What the cross-collector chain does with this output¶
The graph after a successful agenthound loot --type litellm run contains:
(:LiteLLMGateway:AIService) -[EXPOSES_CREDENTIAL]-> (:Credential type=master_key, value_hash=H1)
(:LiteLLMGateway:AIService) -[EXPOSES_CREDENTIAL]-> (:Credential type=apiKey, provider=openai, value_hash=H2)
(:LiteLLMGateway:AIService) -[EXPOSES_CREDENTIAL]-> (:Credential type=apiKey, provider=anthropic, value_hash=H3)
(:LiteLLMGateway:AIService) -[EXPOSES_CREDENTIAL]-> (:Credential type=virtual_key, value_hash=H4)
If you previously ran the Config Collector against an MCP client config that referenced the master key by env var (e.g. LITELLM_MASTER_KEY in claude_desktop_config.json), the Config Collector emitted:
The cross_service_credential_chain post-processor joins on value_hash, walks (:AgentInstance)-[:TRUSTS_SERVER]->(:MCPServer)-[:HAS_ENV_VAR]->(c1) and matches c1.value_hash to a master :Credential exposed by a :LiteLLMGateway, then emits (:AgentInstance)-[:CAN_REACH]->(c2) for every upstream provider Credential c2 the gateway exposes.
The pre-built litellm-credential-leak query surfaces this finding in the UI:
curl -s localhost:8080/api/v1/analysis/findings | \
jq '.[] | select(.processor=="cross_service_credential_chain")'
The result is a one-line agent → MCPServer → env-var-cred → LiteLLM master → upstream OpenAI/Anthropic/Bedrock provider key path that is otherwise invisible to single-collector tools.
Operational notes¶
/key/list 401 is normal. Many production LiteLLM deployments scope down master-key permissions or disable the endpoint entirely. The looter records the 401 in PartialErrors and continues with the /model/info results.
/model/info shape varies across LiteLLM versions. The looter parses leniently — unknown fields are ignored, missing litellm_params.api_key falls back to a synthesized value_hash over (provider, model_name) so the upstream Credential node is still deterministic across re-runs. Schema drift produces no Credential nodes; partial drift produces fewer.
Synthetic value_hash for unknown upstream values. When LiteLLM's /model/info does NOT expose the actual upstream key (the common case — most production deployments mask it), the looter still emits a :Credential node with value_hash = HashCredentialValue(provider + ":" + model_name). This means the upstream Credential node is identifiable across re-runs but the cross-collector chain join only fires for the master key (which the operator supplied) — not for the masked upstream keys themselves. Reasoning: a synthetic hash for upstreams keeps the graph node-stable; a real cross-collector merge for upstreams would require the operator to supply each upstream value, which they don't have (that's the point of master-key compromise).
--include-credential-values=true audit-mode. The default loot is hash-only. With this flag, raw values land in the value property on every :Credential node. Use this for engagements where the deliverable explicitly includes the credentials themselves (e.g. internal red-team handoff to remediation). The cross-collector chain works the same way either way — value_hash is always populated.
Engagement-id is recorded everywhere. Every edge's evidence map carries it; every slog line carries it; the top-level meta.extra.engagement_id carries it. After-the-fact attribution works regardless of which surface the SOC inspects first.
See also¶
- Network scanner — the network scanner that discovers the LiteLLM gateway in the first place.
modules/litellmloot/looter.go— implementation reference.server/internal/analysis/processors/cross_service_credential_chain.go— the post-processor that consumes this output.