Sergen Tanguc
Selected Works
// Case Study

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.

Rust Proxy Security MITM PII LLM Tokio Axum
View on GitHub

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 log

Architecture

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 AAAAAAAAAAAAAAAA would 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, secrets
  • detect-secrets — various credential patterns
  • secrets-patterns-db — community-maintained secret patterns
  • nosey-parker — additional coverage
  • hand-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 CategoryReplacement StrategyExample
Emailfaker_emailalice@example.com
Phonefaker_phone+1-555-010-4821
SSNfaker_ssn000-00-3847
Credit cardfaker_cc_luhnLuhn-valid random card
IBANfaker_ibanGB00BLEEP0000000000000
AWS access keyfaker_aws_keyAKIABLEEP... (20 chars)
GitHub PATfaker_github_patghp_BLEEP... (40 chars)
JWTfaker_jwtstructurally valid JWT
DB connection stringfaker_db_connpreserves host, replaces credentials
Numeric PII (INSEE, PESEL)fpe_numericformat-preserving encryption (AES-FF1)
Genericgeneric_randomsame 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 replacement
  • application/x-www-form-urlencoded — value-level
  • text/plain, text/event-stream (SSE) — full-body scan with per-frame processing for SSE
  • multipart/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.