Skip to content

docs: add an MCP security guide and point the README at it - #29

Merged
griff1 merged 5 commits into
mainfrom
griff1/docs-mcp-security
Aug 2, 2026
Merged

docs: add an MCP security guide and point the README at it#29
griff1 merged 5 commits into
mainfrom
griff1/docs-mcp-security

Conversation

@griff1

@griff1 griff1 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

First of the GitHub SEO work, after setting the repo description and topics.

The gap

The README answers "which package do I install" and "how do I publish". Both useful, neither a question anyone types into a search box or an assistant. Nothing in this repo answered "how do I secure an MCP server against tool poisoning", which is exactly the query cluster these packages serve and where the 2026-07-31 AI answer baseline showed us absent from all 20 unbranded answers.

That baseline also showed what gets cited for these queries: vendor product and docs pages plus standards bodies (genai.owasp.org, modelcontextprotocol.io, learn.microsoft.com, arXiv). Zero review sites, zero listicles. Own docs with explicit standards alignment is the shape that wins here.

What this adds

docs/mcp-security.md, about 1,300 words:

  • The failure modes at the tool boundary, each as a self-contained definition: tool poisoning (including definitions that change after approval), prompt injection that becomes a tool call, excessive agency, confused deputy, exfiltration by chaining, unbounded consumption
  • Why WAF and API-gateway controls cannot see them, argued rather than asserted: for stdio there is no network hop, and for HTTP every one of these is legitimate traffic used wrongly
  • A vendor-neutral table of what to record and why each field carries signal
  • An OWASP mapping (ASI01, LLM06, LLM10), with the caveat that the agentic list is newer and still moving
  • What cerberus-mcp does, and three plain statements of what it does not

The README intro now leads with what the packages give you rather than describing itself as a monorepo, and links the guide from a new Guides section above Packages.

Notes

Deliberately honest about limits, because that is what got our glossary cited in the first place: the guide says gateway instrumentation trades observed arguments for reach, that the package is a sensor rather than detection, and that read-only tools are the first half of an exfiltration chain rather than a reason to skip recording.

All 26 relative links verified. No em-dashes in new content. Checked against the backend table and column list from the hygiene sweep: nothing internal.

The README answers "which package do I install" and "how do I publish".
Both are useful and neither is a question anyone types. Nothing in this
repo answered "how do I secure an MCP server", which is the query cluster
these packages exist to serve and where we are currently absent.

docs/mcp-security.md covers the failure modes at the tool boundary (tool
poisoning, injection that becomes a tool call, excessive agency, confused
deputy, exfiltration by chaining, unbounded consumption), why WAF and
gateway controls cannot see them, what to record if you want to detect
them, and the OWASP mapping.

Written to be useful whatever you instrument with. The what-to-record
table names no vendor, the OWASP section says the agentic list is still
moving, and the cerberus-mcp section states its limits plainly: it
observes a server rather than a model, gateway coverage trades arguments
for reach, and the package is the sensor rather than the detection.

The README intro now leads with what the packages give you instead of
describing itself as a monorepo, and links the guide.
@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review: docs: add an MCP security guide and point the README at it

Docs-only PR (README intro/nav + new docs/mcp-security.md, 183 lines). Well-written and well-scoped — no touching of package code, no internal cerberus-int details leaked, all relative links resolve, no em-dashes in the new content. One accuracy issue worth fixing before merge, plus a couple of minor nits.

Accuracy issue: source-IP pseudonymization is stated as unconditional, but it isn't

docs/mcp-security.md says:

Arguments and headers are sanitized by key name through cerberus-core before anything is sent, and source IPs are HMAC-SHA256 pseudonymized.

and the new README line makes the same claim repo-wide:

Sensitive values are redacted and PII is pseudonymized in the client, before anything is transmitted.

In cerberus-mcp/src/cerberus_mcp/server.py (and the equivalent path in cerberus-django), IP hashing only happens if self._secret_key and source_ip != "mcp-local". Without a secret_key in cerberus_config, the source IP is sent in plaintext, and the code just logs a one-time warning ("Sending source IP in plaintext — no secret_key configured."). Key/value redaction (sanitize_dict) is unconditional, but the IP-hashing half of the sentence is opt-in, not a guarantee.

This matters because the doc's own config example a few lines above omits secret_key:

mcp = CerberusMCP("my-server", cerberus_config={"token": "...", "client_id": "...", "ws_url": "wss://..."})

A reader who copies that snippet gets plaintext source IPs, directly contradicting the sentence that follows it. Given the PR's stated goal of being "deliberately honest about limits," this is the one place the guide overstates a guarantee — worth either adding secret_key to the example or qualifying the sentence (e.g. "...and source IPs are HMAC-SHA256 pseudonymized when secret_key is configured"). The README intro line has the same issue, though it echoes phrasing already present elsewhere in the pre-existing README table, so it's lower priority.

Minor / non-blocking

  • The FAQ and "What cerberus-mcp does" claims (bounded queue with drop-on-full, async transmission off the handler thread, schema report once per startup, session/client identity via WeakKeyDictionary) all check out against the current server.py/transport.py implementation — good.
  • OWASP mapping (ASI01 / LLM06 / LLM10) matches the current category names, and the doc appropriately hedges that the agentic list ("ASI01") is still moving — good call not overclaiming stability there.
  • No test coverage needed/expected for a docs-only change; nothing here touches code paths covered by existing suites.

Overall: solid, accurate-by-default writing with a clear non-vendor-locked framing (the "what to record" table and OWASP mapping apply regardless of instrumentation choice). Fix the IP-pseudonymization overstatement and this is good to merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6cbebf64c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/mcp-security.md Outdated
Comment thread README.md Outdated
Comment thread docs/mcp-security.md Outdated
Comment thread docs/mcp-security.md
griff1 added 3 commits August 2, 2026 18:26
The guide said gateway spans carry tool names without arguments, full
stop, and that server-side instrumentation was the only way to see
arguments at all. The first half is true only of MCP spans. The gateway's
LLM spans carry the model's requested tool calls, and the bridge already
reconstructs them onto the LLM event by default, so a gateway does give
argument-level visibility on Anthropic including streaming, and on
OpenAI non-streaming and the Responses API.

The real limit is narrower and worth stating precisely: OpenAI streaming
loses tool-call arguments upstream, and streaming is what most agent
traffic uses.

Adds a section separating requested arguments from observed ones, with
the per-path coverage table, and notes that a gap between the two is
itself a signal that something rewrote the call.
A per-provider coverage table on a page about securing MCP servers is
the wrong place for it. Three lines and a pointer to the gateway README,
which already documents this properly.
Keeps the row, so anyone deploying against streaming OpenAI knows tool
arguments will not be there rather than discovering it by debugging.
Drops the upstream mechanism, the note about how much traffic it
affects, and the steer toward other backends.

The scope stays, since knowing Anthropic and non-streamed OpenAI are
unaffected is what makes the row actionable.
@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review

Well-written, well-scoped docs addition — clear structure, no fluff, and the "why network controls miss this" and requested-vs-observed-arguments sections are genuinely useful rather than marketing copy. Link check confirms all 26 relative links resolve, and there are no em-dashes introduced in the new content. The gateway argument-capture claims (Anthropic streaming captured, OpenAI streaming lost, CERBERUS_CAPTURE_LLM_CONTENT on by default) match mapper_llm.py and the existing cerberus-envoy-ai-gateway/README.md limits table, and no backend implementation details are leaked per the repo public-repo policy.

Two accuracy issues in the "What cerberus-mcp does" section (docs/mcp-security.md) worth fixing before merge, since this guide is explicitly framed as "honest about limits":

  1. "Arguments and headers are sanitized by key name"cerberus-mcp never captures headers; every emitted event sets headers=None (server.py:371, server.py:450), and structs.py:27 documents this explicitly as "headers: None (MCP has no HTTP headers)". There is nothing being sanitized because there is nothing captured. Suggest dropping "and headers" here, or rephrasing to something like "Arguments are sanitized by key name... (MCP has no HTTP headers to capture)."

  2. "source IPs are HMAC-SHA256 pseudonymized" stated as unconditional — in server.py:433-440, hashing only happens if self._secret_key is configured; otherwise the IP is sent in plaintext and the package logs a warning ("Sending source IP in plaintext — no secret_key configured"). Given the guide stated goal of being precise about limits (it already carves out the OpenAI-streaming gap and the server-vs-gateway argument distinction), this one should probably get the same treatment — e.g. "source IPs are HMAC-SHA256 pseudonymized when secret_key is configured (otherwise sent in plaintext, with a warning)." This same unconditional phrasing already exists in the pre-existing README (line 40), so it is not a new pattern introduced here, but this new guide is the first place it is stated in a security-guarantees context, which raises the bar.

Neither blocks merge on its own, but both are one-sentence fixes, and this doc is likely to get cited/quoted externally per the PR own SEO goal — worth getting the guarantees exactly right.

No test coverage concerns — this is docs-only, nothing executable changed.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review

Docs-only PR — new docs/mcp-security.md (~1300 words) plus README updates linking to it. Overall well-written and appropriately scoped; verified all relative links resolve and no internal cerberus-int/cerberus-ml implementation details leaked. Two accuracy issues worth fixing before merge, since this doc makes specific technical claims about cerberus-mcp behavior that readers (including prospects doing exactly the security research this doc targets) may take at face value.

Issues

1. "source IPs are HMAC-SHA256 pseudonymized" overstates the guarantee (docs/mcp-security.md, "What cerberus-mcp does" section)

This is stated unconditionally, but in cerberus-mcp/src/cerberus_mcp/server.py:433-439, hashing only happens if self._secret_key is configured. Without secret_key set, the code sends the source IP in plaintext and even logs a warning about it:

if self._secret_key and source_ip != "mcp-local":
    source_ip = hash_pii(source_ip, self._secret_key)
elif source_ip != "mcp-local" and not self._warned_no_secret_key:
    ...
    logger.warning(
        "[CerberusMCP] Sending source IP in plaintext — no secret_key configured. "
        "Add secret_key to cerberus_config to enable PII hashing."
    )

Given this doc's whole thesis is "sanitize before transmission, not after storage" and it explicitly calls out pseudonymization as a safety practice, stating the guarantee as unconditional is the kind of claim that gets quoted back at Cerberus later. Suggest: "source IPs are HMAC-SHA256 pseudonymized when secret_key is configured (recommended; otherwise the IP is sent in plaintext)."

2. "Arguments and headers are sanitized by key name" is inaccurate — cerberus-mcp never has headers (same section)

cerberus_mcp/structs.py:27 documents headers: None (MCP has no HTTP headers), and server.py always passes headers=None (lines 371, 450). There's nothing to sanitize because the field is always empty for this package — headers only exist on the cerberus-django side. Recommend dropping "and headers" from this sentence, or rephrasing to make clear it's arguments-only for cerberus-mcp (the gateway/Django packages are the ones with headers).

Minor / non-blocking

  • The docs/mcp-security.md FAQ and "What cerberus-mcp does" sections read well and the honesty about limits (sensor vs. detection, gateway argument-capture tradeoffs) matches the existing tone in cerberus-envoy-ai-gateway/README.md.
  • OWASP mapping table looks accurate as of the current ASI/LLM Top 10 drafts; the caveat that the agentic list "is newer and less settled" is a reasonable hedge given it's still in flux.
  • README intro rewrite is a clear improvement — leads with what the packages give you rather than "monorepo," consistent with the PR's stated SEO goal.
  • No test coverage implications — this is docs-only, nothing to add there.

Nice work on the substance; the two accuracy points above are worth a quick fix given this doc is explicitly meant to be cited externally.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review: docs: add an MCP security guide and point the README at it

Docs-only PR (README + new docs/mcp-security.md, ~1,300 words). Overall this is well-written, well-scoped, and consistent with CLAUDE.md's public-repo rules (wire contract described, no backend internals, no roadmap for defeating telemetry).

What checked out

  • All relative links resolve: cerberus-mcp/README.md, cerberus-core/README.md, cerberus-envoy-ai-gateway/README.md all exist.
  • No em-dashes in the new content (README's pre-existing em-dash on line 1 is untouched).
  • Claims about cerberus-mcp internals match the actual code:
    • Bounded queue with drop-on-full (transport.py: Queue(maxsize=EVENT_QUEUE_MAXSIZE), except thread_queue.Full: logger.warning(...)) - matches "the queue drops rather than blocking."
    • Schema report via _tool_manager / _resource_manager / _prompt_manager, emitted once as mcp_schema_report - matches server.py.
    • Arguments sanitized via sanitize_arguments before transmission - matches "sanitized by key name."
  • Nothing in the new doc reads as internal cerberus-int implementation detail (table/column names, queue internals, etc.) - stays at the wire-contract level as required.

One accuracy issue worth fixing

  • "source IPs are HMAC-SHA256 pseudonymized" (docs/mcp-security.md, "What cerberus-mcp does" section) is stated unconditionally. In cerberus-mcp/src/cerberus_mcp/server.py:433-439, hashing only happens when self._secret_key is configured; without a configured secret_key the source IP is sent in plaintext (with a logged warning). The same overstatement already exists in the untouched README line 40 ("PII ... is pseudonymized with HMAC-SHA256"), so it's a pre-existing pattern rather than something this PR introduces - but since the whole point of this page is to be a precise, citable reference, it would be worth tightening this one line to something like "pseudonymized with HMAC-SHA256 when a secret_key is configured" so the doc does not overstate a default-off protection.

Minor / non-blocking

  • The OWASP mapping table cites ASI01 Agent Goal Hijacking from the "OWASP Top 10 for Agentic Applications 2026" - I was not able to verify the exact code/name against the live site in this review (no web access), and the PR body itself flags this list as "newer and still moving." Worth a final glance at genai.owasp.org right before merge in case the numbering shifted between when the baseline research was done and now.
  • The FAQ claim "the queue drops rather than blocking the server" is accurate for cerberus-mcp, but the doc frames it as a general property of the sensor approach; if cerberus-envoy-ai-gateway's bridge behaves differently under load, that is not visible from this page (not a defect, just noting the generalization is implicitly scoped to cerberus-mcp).

Test coverage
N/A - pure documentation change, no code paths touched. No tests needed.

Summary
Solid, accurate guide with one small correction (conditional vs. unconditional IP hashing) that is easy to fix in one line. No security or structural concerns with the PR itself.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4c3032ff5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/mcp-security.md
Comment thread docs/mcp-security.md
All four from Codex on #29, all verified against the source before
changing anything. They share a shape: the docs promised a guarantee
where the code offers a default.

1. Source-IP pseudonymization is conditional on `secret_key`
   (server.py:433). Without it the IP is normalized and sent in the
   clear behind a single log warning. The guide claimed it flatly, and
   its config example omitted the key, so following the example exactly
   would export the PII you thought you were hashing. The key is now in
   the example, in the guide, and in cerberus-mcp's config table, where
   it was missing entirely. That omission was the root cause.

2. Redaction matches key names against SENSITIVE_KEYS, not values
   (sanitization.py). A secret under an unremarkable key is transmitted
   as-is. Both READMEs said "sensitive values are redacted" without
   qualification. Now stated as what it is: a safety net for the usual
   suspects.

3. Session and client identity only populate for handlers that declare
   a FastMCP `Context` parameter (server.py:208). Handlers without one
   still emit call records, but nothing to group them by, which is what
   the sequence-level detection depends on.

4. Server-side telemetry has the declared tool inventory and the
   observed calls, not the user's task. It shows undeclared tools,
   definitions that changed after approval, and calls outside the
   declared surface. Comparing against what the user actually asked for
   needs intent telemetry from the LLM side.

Point 2's unconditional phrasing also predates this branch, in the root
README's "How they fit together". Fixed there too.
@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review

Docs-only PR (new docs/mcp-security.md + README updates). I cross-checked the technical claims against the actual source rather than just reading prose, and everything holds up.

Verified against source

  • secret_key / plaintext IP warning (cerberus-mcp/README.md, guide): server.py does gate on self._secret_key, uses _warned_no_secret_key to log exactly once, and the warning text matches what's described. ✔️
  • Key-name-only redaction: cerberus-core/sanitization.py matches key.lower() in SENSITIVE_KEYS, no value inspection — matches the guide's "not a guarantee that nothing sensitive can leave" caveat. ✔️
  • Bounded queue, drop-on-full: transport.py uses queue.Queue(maxsize=EVENT_QUEUE_MAXSIZE) and drops with a warning on queue.Full rather than blocking — matches the "observability path that can stall a tool call is worse than one that loses events" FAQ answer. ✔️
  • Context-based session identity: server.py does lazily import mcp.server.fastmcp.Context and extracts identity only when a handler takes one — matches "handlers without one still produce call records, but with no session to group them by." ✔️
  • All 26 relative links (../cerberus-mcp/README.md, ../cerberus-core/README.md, ../cerberus-envoy-ai-gateway/README.md, ./docs/mcp-security.md, etc.) resolve to files that exist in this checkout.

CLAUDE.md compliance

  • No internal cerberus-int/cerberus-ml implementation details (table names, column names, internal class names) appear anywhere in the new content — it stays at the wire-contract / behavior level throughout, consistent with the repo's public-repo policy.
  • Nothing here reads as a "how to defeat the telemetry" map; if anything the piece argues the opposite (why WAFs miss this class of attack) without giving evasion recipes.

Content quality

  • Genuinely well-scoped: each failure-mode section stays a self-contained definition rather than sprawling into implementation, and the "what cerberus-mcp does / does not do" framing at the end is honest about limits (sensor-not-detection, key-name-only redaction, gateway-vs-server argument-observation tradeoff) rather than oversold — this fits the project's existing tone (compare the honesty of the cerberus-envoy-ai-gateway gaps table, which this PR also touches).
  • The OWASP mapping table is reasonable and appropriately hedged ("agentic list is newer and less settled... expect the numbering to move") given ASI01 is a 2026 draft list, not a finalized standard.

Minor / non-blocking suggestions

  1. README "How they fit together" wording: the new sentence "Redaction matches key names against SENSITIVE_KEYS, so it catches the usual suspects rather than inspecting values" is a good, honest addition — worth double-checking it doesn't read as walking back the earlier PII/HMAC sentence in the same bullet; on a read-through it's fine, just flagging since it's a meaningful behavioral disclosure landing in the top-level README rather than only in the new guide.
  2. docs/mcp-security.md "What to record" table: solid and vendor-neutral as claimed. One nit — "Session and client identity" row could optionally note (as the cerberus-mcp section later does) that this is FastMCP-Context-dependent, since a reader skimming just the table might assume it's unconditional. Not necessary since the caveat does appear later in the doc.
  3. Streamed-OpenAI row edit in cerberus-envoy-ai-gateway/README.md: the rewrite ("Not currently captured...") is more concise than the prior version but drops the explicit backend-preference recommendation ("prefer Anthropic backends or non-streamed OpenAI until this is fixed upstream"). If that recommendation was still considered useful operator guidance, consider keeping a short version of it; if it was cut deliberately (e.g., no longer accurate, or redundant with the new guide), no action needed.

Risks

None found. This is additive documentation with no code or behavior changes; the only "risk" is prose accuracy, which checks out against the current implementation. Nothing here touches sanitization logic, parity-fixtures, or the SENSITIVE_KEYS set, so no fixture updates are required.

Nice work — this is a good example of documentation that discloses real limitations (key-name-only redaction, no secret_key = plaintext IPs, sensor-not-detection) rather than glossing over them, which lines up with the repo's stated bar of not overselling what these packages do.

@griff1
griff1 merged commit 3119238 into main Aug 2, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09feca8aa4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md
This repository is a monorepo of the **client-side** Cerberus integrations — the libraries and gateway policy you add to your application (or place in front of it) to send request metrics to Cerberus. Each integration targets a different runtime but emits the **same event schema**, so they're interchangeable from the backend's point of view.
These are the libraries and gateway policies you add to your application, or place in front of it, so that a request, an MCP tool call, or an LLM call becomes something you can see. Each integration targets a different runtime and they all emit the same event schema, so they are interchangeable and can be mixed in one deployment.

Values under known sensitive key names are redacted in the client, and source IPs are HMAC pseudonymized when a secret key is configured, both before anything is transmitted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude unsanitized schema reports from the redaction promise

The new key-name qualification still fails when a tool schema contains a sensitive default or example under a known key: _report_schema() copies descriptions and input_schema directly into custom_data and queues the event without calling sanitize_arguments() or sanitize_dict() (cerberus-mcp/src/cerberus_mcp/server.py:295-300,355-378). In that scenario the value leaves the client unredacted despite this guarantee, so either sanitize schema-report metadata or explicitly exclude it from the promise.

Useful? React with 👍 / 👎.

Comment thread docs/mcp-security.md
Comment on lines +141 to +142
- **Redaction is by key name.** Arguments and headers pass through
[`cerberus-core`](../cerberus-core/README.md), which redacts values whose

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not claim MCP request headers pass through sanitization

For HTTP/SSE MCP handlers, _emit_event() hardcodes headers=None (cerberus-mcp/src/cerberus_mcp/server.py:450), so request headers neither pass through cerberus-core nor reach the emitted telemetry. This wording can make operators expect header visibility and redaction that the integration does not provide; limit the statement to arguments.

Useful? React with 👍 / 👎.

Comment thread docs/mcp-security.md
Comment on lines +202 to +203
Instrument the gateway in front of them. You keep the call graph and the
arguments the model requested. What you give up is confirmation that the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Qualify gateway argument retention

For streamed OpenAI traffic, the linked gateway README states that LLM tool-call arguments are not captured, while its MCP spans carry only the tool name, so deploying the gateway does not preserve “the arguments the model requested” in that scenario. Qualify this FAQ answer to the provider/streaming modes that actually expose arguments instead of presenting it as an unconditional property of gateway instrumentation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant