Skip to content

security(keys): key-leak audit + SECURITY.md threat model#44

Closed
ernestprovo23 wants to merge 4 commits into
mainfrom
sec/v1-keyleak-audit-threatmodel
Closed

security(keys): key-leak audit + SECURITY.md threat model#44
ernestprovo23 wants to merge 4 commits into
mainfrom
sec/v1-keyleak-audit-threatmodel

Conversation

@ernestprovo23

Copy link
Copy Markdown
Member

PR-B of the conclave v1.0 release: KEY-LEAK AUDIT + THREAT MODEL

Backs conclave's headline BYO-keys / key-rigor claim. Audits + fixes the residual key-leak surface a security reviewer probes day 1, with a regression test per vector, and adds a Threat model section to SECURITY.md.

Per-vector summary

Vector Finding Resolution Test
1 Cache write path Cache serializes only the already-redacted CouncilResult; key never in file/filename/key. Under-tested. Protected by design + invariant comment in cache.store; added end-to-end planted-secret test. V1
2 Streaming chunk path Deltas are answer content; mid-stream errors redacted at providers boundary, never streamed. Protected; documented transport boundary; added planted-secret stream test. V2
3 repr/str No adapter/config/result object stores a key; transient headers not retained. Protected; added repr/str/json assertions. V3
4 Provider 400/422 echo status_error -> ProviderError redacts on construction; call path redacts at capture. Protected; added buffered echo tests incl. unprefixed custom-endpoint key. V4
5 httpx/httpcore DEBUG logging Out-of-band of redact() (HIGH if enabled by host app). Accepted limitation, documented loudly + opt-in guard_transport_logging(). V5
6 redact misses URL-encoded/partial fragments redact() is best-effort on shapes; value-pass is primary. Accepted limitation, documented honestly. (doc)
7 Test fixtures All obviously-fake (...FAKE...). Protected; .gitleaks.toml allowlists test tree only. V6
8 Partial-failure catch-alls (AUDIT-FOUND, not in original map) Council.fan_out + streaming._drive_member built error strings from raw exception text without redact(). FIXED: both now redact(). V7

Invariants

  • Tests 191 -> 209; coverage 89%+ (floor 75 enforced).
  • ruff check + ruff format clean; gitleaks: fake fixtures allowlisted (test tree only).
  • SECURITY.md disclosure policy unchanged; only a Threat model section added.
  • No real secrets. No Co-Authored-By.

DO NOT MERGE — pending dogfood review by conclave's own adversarial council on audit completeness.

Audit and harden the BYO-keys leak surface ahead of v1.0, with regression
tests per vector (tests/test_keyleak_audit.py):

- cache write path: assert a planted key-shaped secret echoed in a provider
  error never reaches a cache file/filename/key (cache stores the already-
  redacted CouncilResult); add an explicit invariant comment in cache.store.
- streaming path: assert a mid-stream provider error echoing a key is absent
  from every streamed StreamEvent AND the final ModelAnswer; document the
  transport->providers redaction boundary in stream_sse.
- __repr__/__str__: assert no adapter/config/result object renders key
  material (no object stores a key; transient request headers are not
  retained).
- provider 400/422 echo: assert buffered error capture runs through redact()
  for prefixed and unprefixed custom-endpoint keys.
- httpx/httpcore DEBUG logging (out-of-band of redact): document loudly and
  add an opt-in guard_transport_logging() helper that drops transport DEBUG
  records (the only level that emits auth headers).
- audit-found gap (not in the original attack map): the partial-failure
  catch-alls in Council.fan_out and streaming._drive_member built error
  strings from raw exception text without redact(); wrap both in redact().

Add a Threat model section to SECURITY.md (trust boundary, what IS protected,
accepted limitations, vector map) without touching the disclosure policy. Add
.gitleaks.toml allowlisting the obviously-fake test fixtures (test tree only).

Tests 191 -> 209; coverage 89%+. Disclosure policy unchanged.
@ernestprovo23 ernestprovo23 force-pushed the sec/v1-keyleak-audit-threatmodel branch from cab6334 to 4edecae Compare June 14, 2026 17:20
…ture

The two handler-count assertions in test_logging.py counted ALL handlers on
the conclave logger, which fails under newer pytest (9.x) whose logging plugin
injects LogCaptureHandler instances. Filter to conclave's own StreamHandler via
a _app_stream_handlers helper (excludes any handler whose class name contains
"Capture") so the tests assert on conclave's one-shot config without coupling to
the pytest version. Passes under pytest 8.4.2 and 9.1.0. Test-only; no source
change. Unblocks CI for the key-leak audit PR.
…hain (RANK 1/5)

The httpx exception raised on a transport failure carries a live .request
whose .headers hold the Authorization/x-api-key value. Surfacing it as
TransportError.__cause__/__context__ left the key one cause-chain hop away,
leaking under traceback.format_exception, logging.exception, or a cause-chain
repr. The four raise sites in post_json/stream_sse now route through
_raise_transport_error (raise ... from None) plus a boundary clear that nulls
__context__, so neither a formatter nor a direct attribute walk can reach the
header-bearing exception. Pinned by V8 tests.
…t-out (RANK 6)

Council.__init__ now calls transport.guard_transport_logging() automatically
unless allow_transport_debug_logging=True, so a process holding a real key is
protected from httpx/httpcore DEBUG header leakage out of the box. The guard is
idempotent and scoped to the httpx/httpcore loggers only; it never touches the
host root logger. SECURITY.md reworded from opt-in to default-on/opt-out, with a
new What-IS-protected bullet and vector-map rows for the cause-chain hardening
(RANK 1/5) and the default-on guard (RANK 6). Adds V9 (guard default-on +
opt-out) and V10 (client close hygiene, RANK 8) regression tests.
@ernestprovo23

Copy link
Copy Markdown
Member Author

Superseded by #45 (integrated into the v1.0.0 release commit).

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