Keep secrets out of LLM traffic—without losing context.
Sensitive Content Replacement, Unmasking, Brokering & Rehydration — a single-binary forward proxy that masks secrets / PII / sensitive data on outbound requests to LLM providers and rehydrates (unmasks) it on the inbound response, including mid-stream.
The provider only ever sees opaque placeholders; your users receive fully reconstituted responses. SCRUBR is not an LLM gateway for routing/cost — it owns the payload and guarantees a lossless, reversible de-identification round trip. The wedge is security & compliance (SOC 2, PCI-DSS, HIPAA, GDPR).
┌────────┐ original ┌────────┐ masked ┌──────────┐
client ─┤ app ├──────────────►│ SCRUBR ├─────────────►│ LLM API │
└────────┘ (secrets) └────────┘ (⟦S:…⟧ ids) └──────────┘
▲ rehydrated │ reverse map (in-mem / Redis) │
└────────────────────────┴───────────────────────────────┘
- Website: a generated docs/guides site lives in
website/(shadcn-styled, static, deployed to GitHub Pages). - For AI agents: an agent skill describes how to self-install and operate
SCRUBR; the site also serves
/llms.txtand/llms-full.txt. - Docs: Configuration · Deployment & Ops · Use as an HTTP proxy · Kubernetes (Helm) · Security & Threat Model · Design · Changelog
- Example configs:
scrubr.example.yaml·examples/proxy.yaml(HTTP-proxy) ·examples/common-rules.yaml(curated rules)
- Detect — on the request, SCRUBR scans the configured JSON content paths
(
messages[].content, …) using a glossary (Aho-Corasick), a single-pass regex meta-engine, an optional entropy catcher, secret-store values, and an optional heuristic NER. - Mask — each detected span is replaced by a reversible sentinel
⟦S:TYPE·id·tag⟧. Theidindexes a reverse table held only in SCRUBR; the secret never leaves. Equal originals dedupe to the same id (stable pseudonyms). Thetagis a per-vault keyed MAC, so only sentinels SCRUBR actually issued rehydrate — a hostile upstream can't forge⟦S·0⟧,⟦S·1⟧, … to read the vault. - Forward — the masked request goes to the configured upstream.
- Rehydrate — as the response streams back, SCRUBR splices the originals back in. It is
correct at every byte boundary and reassembles a sentinel fragmented across SSE
data:events (real LLM token streaming). - Wipe — request-scoped reverse maps are
zeroized at response end; session maps expire by TTL.
The reverse table is per-request by default, or per-session (stable pseudonyms across a multi-turn conversation), backed by memory or Redis.
cargo run --bin scrubr -- --config scrubr.yaml --listen 127.0.0.1:8080# scrubr.yaml
routes:
- { listen_path: "/openai", upstream: "https://api.openai.com", profile: openai }
profiles:
openai:
scan_paths: ["messages[].content"]
stream_paths: ["choices[].delta.content"] # required for streaming responses
rules:
- { name: email, type: EMAIL, pattern: '[\w.+-]+@[\w.-]+\.\w+', priority: 50 }Point your app at http://scrubr:8080/openai/v1/chat/completions. The upstream sees masked
content; your app gets the rehydrated stream. Start with masking.mode: dry-run to validate
detection coverage before enforcing.
Prefer to set SCRUBR as your OS/app HTTP proxy (no base-URL change)? See
docs/HTTP-PROXY.md — ./scripts/setup-ca.sh ca then
scrubr --config examples/proxy.yaml.
cargo run --bin scrubr demo # offline mask → streamed echo → rehydrate
cargo run --bin scrubr -- --version
cargo test # full test suite- Glossary (literal terms) and regex rules compiled into one
regex-automatameta-engine — a single pass whose cost is ~flat in rule count. - Curated ruleset for popular secret formats (AWS/GCP/DigitalOcean keys; GitHub/GitLab/
Slack/Stripe/SendGrid/Twilio/npm/OpenAI/Anthropic tokens; JWTs; PEM private keys;
credential URLs; bearer tokens; generic assignments) in
examples/common-rules.yaml. - Entropy detector for high-entropy secrets no named rule covers (opt-in).
- Heuristic NER for person-name PII behind a pluggable
SpanDetectorseam (opt-in; a model-backed detector can replace it via the same trait). - Secret sources feed values into detection at startup/reload:
.env, secret files, and HashiCorp Vault (KV v2). - Provider-aware scan paths — mask only content, never
model/metadata. Deterministic overlap resolution by priority.
- Reversible sentinel masking with reverse-table indices (secret never leaves SCRUBR).
- Streaming rehydration state machine — lossless at every chunk boundary, JSON-string-safe.
- SSE-aware rehydration — reassembles sentinels fragmented across delta events
(
stream_paths). - Per-request dedup;
zeroize-on-drop vault.
- Request scope (default) or session scope (stable pseudonyms across a conversation, keyed by a request header), TTL-evicted.
- Backends: in-memory (single node) or Redis (cross-node). Each node gets a
disjoint id space and writes per-field Redis hashes, so concurrent nodes never collide
ids or lose entries. Stored vaults are encrypted at rest (AES-256-GCM) with an
encryption_key.
- Dry-run mode — detect and report (
x-scrubr-detectedheader) while forwarding the original, for onboarding/compliance trust. - Per-route policy overrides (mode/scope/style) — e.g. dry-run a canary route.
- Multi-tenant — a client key → tenant with its own policy, private glossary, and isolated session namespace (tenant > route > global precedence).
- Proxy auth — optional API-key gate, compared in constant time; the key is never
forwarded upstream. Unauthenticated
/healthz. - TLS termination — serve clients over HTTPS.
- TLS interception (MITM) — mint a per-host cert on the fly from a configured CA and mask any intercepted HTTPS, in SNI-transparent or CONNECT-proxy mode.
- rustls +
ringthroughout — no OpenSSL/aws-lc.
- Tamper-evident audit log — hash-chained JSONL of detections (counts/types, never
values);
scrubr audit-verify <path>detects any edit/deletion, across rotated segments. - Transaction log — full per-request JSONL of the masked provider-facing request and
response, with a
x-scrubr-request-idcorrelation id; secret-free in enforce mode. - Log rotation — audit/transaction logs chunk by size and/or day with retention; the audit hash chain carries across segments.
- OpenTelemetry traces, metrics, and logs over OTLP/gRPC, plus a Prometheus
/metricsendpoint and/healthz+/readyzprobes — all opt-in viatelemetry. - Export inputs & outputs — optionally ship each transaction (masked request/response) as an OTEL log record for centralized audit; safe body policy, off by default.
- See docs/OBSERVABILITY.md.
- Hot-reload — config + watched secret files recompile and swap atomically; a bad edit keeps the last good config.
- Single static binary, multi-arch; container image. Graceful SIGINT/SIGTERM drain.
The full reference is in docs/CONFIGURATION.md; the annotated
example is scrubr.example.yaml. Top-level sections:
| Section | Purpose |
|---|---|
routes[] |
inbound listen_path (or host for interception) → upstream + profile + optional policy |
profiles{} |
scan_paths (request) / stream_paths (SSE response) per provider |
masking |
global mode / style / scope / ttl / session_header |
rules[], glossary[], entropy, ner |
detection |
sources[] |
.env / secret-file / Vault ingestion |
auth, tenants[] |
proxy authentication and multi-tenant policy |
sessions |
memory / redis backend, encryption_key, node_id |
tls, intercept |
TLS termination / interception |
audit, transactions |
tamper-evident + full transaction logging (with rotation) |
telemetry |
OpenTelemetry (OTLP/gRPC) + Prometheus + log format |
CLI: --config <path>, --listen <addr>, --version, demo, audit-verify <path>.
Env: SCRUBR_CONFIG, SCRUBR_LISTEN, RUST_LOG, SCRUBR_OTLP_ENDPOINT, SCRUBR_LOG_FORMAT, VAULT_TOKEN.
crates/
scrubr-core/ I/O-free engine: config, detect, mask, scan, ner, rehydrate, sentinel, vault (+ benches)
scrubr/ binary + lib:
proxy (listener/router/forward), connect (CONNECT-proxy MITM),
mitm (cert minter), secrets (.env/file/Vault), reload (watcher),
session (backends/TTL), redis_backend, crypto (at-rest),
audit (hash chain), transactions (request/response log), demo CLI
examples/ common-rules.yaml, …
docs/ CONFIGURATION.md, DEPLOYMENT.md
Static single binary, no OpenSSL (rustls + ring). Build all platforms with
scripts/build-release.sh (skips targets whose toolchain isn't installed), or via the
Release GitHub workflow on a v* tag. Supported targets:
| OS | x86_64 | aarch64 |
|---|---|---|
| Linux (glibc) | ✓ | ✓ |
| Linux (musl, static) | ✓ | ✓ |
| macOS | ✓ | ✓ |
| Windows | ✓ (msvc/gnu) | — |
Container — a multi-arch (amd64 + arm64) image is published on each release:
docker run --rm -p 8080:8080 -v "$PWD/scrubr.yaml:/etc/scrubr/scrubr.yaml:ro" \
ghcr.io/scrubr-dev/scrubr:latest --config /etc/scrubr/scrubr.yaml --listen 0.0.0.0:8080Or build from source locally: docker build -t scrubr ..
Kubernetes — a Helm chart is published as an OCI artifact on each release, with
single-node and HA (StatefulSet + Redis, distinct per-pod node_id) modes:
helm install scrubr oci://ghcr.io/scrubr-dev/charts/scrubr --version X.Y.ZProduction-ready. The public API and config schema follow Semantic Versioning.
Apache-2.0 — see LICENSE. Report security issues per SECURITY.md.