ReskSafety · Documentation
ENV Match
Secrets and PII are replaced by stable [[RESK_*]] flags before the payload leaves the firewall, then restored locally in the response — including streamed chunks and tool-call arguments. The provider never receives the original value.
On this page
How it works
user prompt: "mail [email protected], key sk-live-9f2a…"
│
▼ mask (before egress)
provider sees: "mail [[RESK_EMAIL_1]], key [[RESK_SECRET_1]]"
│
▼ model answers, echoing the flags
model output: "Sent to [[RESK_EMAIL_1]] using [[RESK_SECRET_1]]"
│
▼ restore (local, from the per-request vault)
client sees: "Sent to [email protected] using sk-live-9f2a…"
Masking happens in the firewall process, after authentication and policy screening, and immediately before the upstream call. Restoration happens on the way back, after the output policy checks. The mapping is held in memory for the duration of one request.
What is masked
| Kind | Detection | Flag |
|---|---|---|
ENV | Values of protected environment variables: LLM_BACKEND_API_KEY, ANTHROPIC_API_KEY, PROVIDER_ENCRYPTION_KEY, JWT_SECRET_KEY, CSRF_SECRET, RESKPOINTS_API_KEY, RESEND_API_KEY, DATABASE_URL, plus names in ENV_MATCH_ENV_VARS | [[RESK_ENV_1]] |
SECRET | sk-…, sk-ant-…, GitHub, Slack, Stripe, AWS and Google API keys, JWTs, Bearer … tokens, PEM private keys, api_key=…/password=… assignments | [[RESK_SECRET_1]] |
SHA | 32, 40 and 64 character hexadecimal digests (MD5, SHA-1, SHA-256) | [[RESK_SHA_1]] |
EMAIL | Email addresses | [[RESK_EMAIL_1]] |
PHONE | Phone numbers, international or grouped (9–15 digits) | [[RESK_PHONE_1]] |
CARD | Payment card numbers, validated with the Luhn checksum | [[RESK_CARD_1]] |
Values are de-duplicated inside a request: the same secret referenced three times keeps the same flag. Distinct values get distinct indexes per kind.
Round trip details
- System hint. When something was masked, a system message is added: placeholders are opaque strings and must be reproduced verbatim. It is a hint, not a guarantee — see the limits.
- Streaming. A flag split across two SSE chunks (
[[RESK_EM+AIL_1]]) is buffered by a state machine and restored once complete; pending text is flushed when the stream ends. - Tool calls. Streamed
tool_calls[].function.arguments(OpenAI) andpartial_json(Anthropic) are restored, so the local tool receives the real value even though the model only saw a flag. - Tool results. In the batch endpoint, tool outputs are masked like user text, so a tool returning a key or an address does not leak it upstream.
- Logs. The prompt hash and stored prompt use the masked text. The mapping itself is never logged or persisted.
- Images and arbitrary JSON. String values anywhere in the message tree are masked; binary parts are forwarded untouched.
Configuration
| Variable | Default | Effect |
|---|---|---|
ENV_MATCH_ENABLED | true | Master switch |
ENV_MATCH_ENV_VALUES | true | Mask protected environment values |
ENV_MATCH_SECRETS | true | Mask known secret formats |
ENV_MATCH_DIGESTS | true | Mask MD5/SHA hex digests |
ENV_MATCH_PII | true | Mask emails, phones, card numbers |
ENV_MATCH_MIN_SECRET_LENGTH | 8 | Ignore shorter environment values |
ENV_MATCH_MAX_MATCHES | 500 | Safety cap per request |
ENV_MATCH_HINT | true | Add the system hint about flags |
ENV_MATCH_ENV_VARS | empty | Extra environment variable names (comma separated) |
Turning off a category only disables detection for it; masking stays enabled for the others. Typical reasons to narrow the scope: a tool that legitimately needs to forward a phone number, or prompts that contain digests the model must reason about.
Guarantees
- The original value is not sent. The upstream payload is built from masked messages, masked
extraand maskedmetadata. - Vault isolation. Flags are scoped to one request; a flag from another request cannot be resolved, so a malicious model cannot ask the firewall to reveal someone else's value.
- No persistence. The mapping lives in a local object, is never stored in the database, never included in logs and disappears with the request.
- Fail closed. If masking raises, the request is rejected with HTTP 500 instead of being forwarded unmasked.
Limits
- Pattern-based. A paraphrase (“the key I pasted earlier”), a re-encoded secret or an unknown token format is not detected. ENV Match is a control, not a DLP guarantee.
- Model cooperation. Restoration depends on the model echoing the flag. A model that rewrites
[[RESK_EMAIL_1]]into prose loses the value (it is not leaked, but it cannot be restored). Missing brackets and case changes are tolerated. - Tool schemas are not masked.
tools[]is forwarded untouched so JSON-schema validation keeps working; do not put secrets in tool descriptions or enums. - Short environment values. Values below
ENV_MATCH_MIN_SECRET_LENGTHare ignored to avoid masking ordinary words; raise the floor if your secrets are short. - Cost. Every request pays a regex pass over its messages. Very large prompts or thousands of matches hit
ENV_MATCH_MAX_MATCHESand are truncated to the earliest spans.
Try it
curl -s http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer $RESK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Email [email protected] the key sk-abcdefghijklmnop1234567890"}]
}'
With a debug provider or a request log capture, the upstream body contains [[RESK_EMAIL_1]] and [[RESK_SECRET_1]]; the client receives the original address and key in the answer.
Implementation
The layer lives in backend/src/resk_app/services/env_match_service.py and is wired into both firewall routers (OpenAI and Anthropic, streaming and non-streaming, batch tool calls). Tests cover detection, flag stability, vault isolation, streaming splits, tool-argument restoration and an end-to-end request against a mocked provider: backend/tests/test_env_match.py.