Bleep Gateway
Stop leaking PII to Claude, GPT, and every LLM API you call
Transparent MITM proxy that intercepts outbound HTTP/S traffic and replaces sensitive data with format-preserving fakes before it reaches LLM providers or third-party APIs.
The Problem
When you call an LLM API or any third-party service, your HTTP request body leaves your perimeter. If a developer logs a user object for debugging, an analytics SDK captures more than it should, or an error report embeds a request payload, PII ends up in systems you do not control — logs, model training data, third-party dashboards.
The common fix is to audit every log statement and instrument every service individually. In practice this does not scale: teams are inconsistent, languages and frameworks differ, and audits go stale. The problem is a perimeter problem, not a code quality problem.
Bleep sits transparently between your services and the outside world. Every outbound HTTP/S request passes through it. Sensitive data gets replaced with format-preserving fakes before the packet leaves your network.
Approach
Bleep is a transparent MITM proxy implemented in Rust on top of hudsucker — itself built on hyper, rustls, and tokio. The proxy terminates TLS using a local CA certificate, inspects plaintext request and response bodies, applies a detection and replacement pipeline, then re-encrypts and forwards the modified traffic.
No application code changes required. You set HTTPS_PROXY=http://127.0.0.1:9190 and configure your services to trust the proxy CA.
service → bleep proxy (:9190) → external API / LLM provider ↓ detection pipeline (AhoCorasick + per-rule regex + entropy filter) ↓ replacement pipeline (format-preserving fakes) ↓ JSONL audit logArchitecture
Detection Pipeline
Detection is a pure function — no I/O, no side effects, no async. It receives &[u8] and returns a Vec<Match> sorted descending by byte offset, ready for right-to-left splice in the replacement stage.
The pipeline has three layers:
1. Combined pre-filter (AhoCorasick)
An AhoCorasick automaton is built at startup from the keyword lists of all loaded rules. Any body that does not contain at least one keyword is rejected in microseconds before a single regex runs. This is the fast path for the vast majority of traffic.
2. Per-rule regex + entropy + checksum
For bodies that pass the pre-filter, each rule runs its compiled regex::bytes::Regex. Rules can further require:
- Entropy threshold: Shannon entropy of the matched bytes must exceed a minimum. A string like
AAAAAAAAAAAAAAAAwould match a generic 16-char pattern but is not a real secret. - Luhn checksum: For credit card rules, the matched digit sequence must pass the Luhn algorithm. This eliminates false positives like order numbers or timestamps that happen to be 16 digits.
// example: overlap resolution keeps the longer span// "abc xyz" at 0..7 wins over "abc" at 0..3 (contained, dropped)matches.sort_by(|a, b| { a.span.start.cmp(&b.span.start) .then(b.span.len().cmp(&a.span.len()))});3. Overlap resolution
When two rules match overlapping spans, the longer span wins. This prevents double-replacement and ensures a credit card number embedded inside a longer token is handled once by the most specific rule.
Rule Sources
Rules are normalized at build time by build.rs from multiple vendor sources:
gitleaks— API keys, tokens, secretsdetect-secrets— various credential patternssecrets-patterns-db— community-maintained secret patternsnosey-parker— additional coveragehand-authored— PII patterns not covered by secret scanners: French INSEE, German Steuer-ID, Polish PESEL, UK NIN, IBM Cloud IAM
Each rule has a category (secret | pii | infra), a confidence level, and a replacement_type that determines what kind of fake value replaces it.
Replacement Pipeline
Every replacement is format-preserving. The goal is that downstream services continue to function — request bodies remain valid JSON, length constraints are not violated, and type expectations hold. The Redaction struct records both the original and the fake for the audit log.
| Rule Category | Replacement Strategy | Example |
|---|---|---|
faker_email | alice@example.com | |
| Phone | faker_phone | +1-555-010-4821 |
| SSN | faker_ssn | 000-00-3847 |
| Credit card | faker_cc_luhn | Luhn-valid random card |
| IBAN | faker_iban | GB00BLEEP0000000000000 |
| AWS access key | faker_aws_key | AKIABLEEP... (20 chars) |
| GitHub PAT | faker_github_pat | ghp_BLEEP... (40 chars) |
| JWT | faker_jwt | structurally valid JWT |
| DB connection string | faker_db_conn | preserves host, replaces credentials |
| Numeric PII (INSEE, PESEL) | fpe_numeric | format-preserving encryption (AES-FF1) |
| Generic | generic_random | same length, same character class distribution |
The fpe_numeric strategy uses AES-FF1 (the fpe crate) to produce a numerically distinct value with the same digit count. A French INSEE number stays a valid-format 15-digit sequence. The original is recoverable if you hold the key — useful for de-anonymization pipelines.
Same raw value always produces the same fake within a single request (dedup map). If the same email appears three times in a body, all three instances get the same replacement.
Content Router
The content router dispatches to format-specific handlers before calling detection. Format matters because JSON field extraction avoids false positives from context keywords bleeding across fields. Supported content types:
application/json— field-level extraction and replacementapplication/x-www-form-urlencoded— value-leveltext/plain,text/event-stream(SSE) — full-body scan with per-frame processing for SSEmultipart/form-data— per-part extraction
Gzip and deflate-encoded bodies are decompressed before scanning and re-compressed after replacement. The Content-Length header is updated to match the modified body length.
Audit Log
Redactions are written to a JSONL file (bleep.jsonl by default). Each entry records the rule ID, category, severity, the fake value, and the request ID — never the original value. The original stays only on the local disk in the JSONL line (for de-anonymization workflows), not on the event bus.
Signed Request Bypass
AWS SigV4 signs the request body as part of the signature calculation. Modifying the body would invalidate the signature and cause the upstream to reject the request. Bleep detects Authorization: AWS4-HMAC-SHA256 Credential=... headers and forwards signed requests unchanged.
TUI
A bleep-tui binary provides a terminal UI built with ratatui + crossterm that tails the event bus in real time, showing redactions as they happen.
Lessons Learned
Certificate caching matters. Generating a new leaf cert per hostname via rcgen is expensive under load. Hudsucker’s built-in LRU cache (configured to 1000 entries) keeps this negligible for typical traffic patterns.
The pre-filter is the only thing that makes this viable at scale. Running 200+ compiled regexes against every request body is not feasible. The AhoCorasick combined pre-filter means most bodies (those with no keyword hits) never reach regex evaluation at all. The overhead on clean traffic is near-zero.
Format-preserving replacement is harder than redaction. [REDACTED] breaks JSON parsers, length constraints, and downstream type checks. Generating a Luhn-valid credit card number, a structurally valid JWT, or a same-format national ID number requires knowing the format — which is why the replacement_type field exists on every rule rather than a single global redaction strategy.