Skip to main content

Ephemeral Proxy

The Ephemeral Proxy is a one-shot HTTPS proxy. Unlike Routes, there is no pre-configured row — every call is its own invocation. You pass the upstream URL in a header, KnoxCall validates it, swaps any {{ token: ... }} references for the underlying vault values, and forwards the request. The composition with Vaults is the killer feature: store the token in your database, send the token to KnoxCall, KnoxCall sends the raw value to the upstream. On this path the raw SSN, bank detail or API secret is never returned to your systems.
Scope of that guarantee: the proxy never hands the value back to you, but the same credential that can detokenize on the proxy path can also read the value outright via GET /v1/vaults/:vault/tokens/:token. The control is about where values flow, not a ceiling on what a detokenize-capable credential can reach. Binding a vault to allowed destinations, and refusing the plain read for card-shaped vaults, is planned work.

When to use it

Use Ephemeral Proxy when:
  • The destination URL is dynamic — chosen per-request, not per-route.
  • You want to detokenize vault tokens last-mile (PII handling, secret injection).
  • You need a quick passthrough without configuring a Route, ApiKey scope, environment overrides, etc.
Use Routes when:
  • You call the same upstream repeatedly and want signing, rate limits, env overrides, and full per-request observability.
  • You need IP allowlisting, mTLS, or method-specific configuration.
  • You want logs grouped by route in the dashboard.

How a request flows

The token strings, raw values, and bodies are never logged. Only metadata (upstream host, status code, latency, token count) lands in the audit chain and the Fleet Graph.

Quick example

The tok_01HZX... came from a prior POST /v1/vaults/.../tokenize call — see the Vaults overview.

Template syntax

Two reference forms are supported in the JSON body and query string: Templates are scanned in body and query only. Headers pass through unchanged in v1 — header-injection is a separate hardening pass. If a token isn’t found, KnoxCall returns 400 token_not_found rather than silently sending an empty value to the upstream. Anything else brace-shaped that isn’t one of the forms above — {{ secret: x }}, {{ token: }}, a mis-cased {{ TOKEN: ... }} — is likewise refused with 400 template_error rather than forwarded verbatim, so an unresolved template can never reach the upstream in place of a credential.

Required and optional headers

Reserved headers (Authorization, Cookie, Host, Content-Length, anything starting with X-Knox-) are stripped before the request reaches the upstream, and so is X-Api-Key when it holds a KnoxCall credential (a value beginning tk_, kc_, kr_ or AKE) — that is the key you authenticated to KnoxCall with, and forwarding it would put a key granting your whole management API into your upstream’s access logs. An X-Api-Key carrying your upstream’s own key (sk-ant-…, and so on) is forwarded unchanged, so providers that authenticate with that header keep working. The headers KnoxCall reads itself and the headers a proxy in front of KnoxCall adds are not forwarded either: X-KnoxCall-Key, X-KnoxCall-Route, X-KnoxCall-Environment, X-Forwarded-For, X-Real-IP, True-Client-IP, CF-Connecting-IP and the rest of the exact names in Request headers KnoxCall does not forward. Those are matched by exact name, not by prefix. Every other header you send is forwarded unchanged, including a CF-Access-Client-Id / CF-Access-Client-Secret pair for an upstream behind Cloudflare Access.

What you get back

KnoxCall passes the upstream response through verbatim — same status code, same headers (minus hop-by-hop), same body bytes. The one exception is a request that detokenized a card number; see Responses are re-tokenized below. KnoxCall adds two headers so you can correlate: If the upstream is unreachable, KnoxCall returns 502 upstream_error.

Responses are re-tokenized

If your request detokenized a token from a pan vault, KnoxCall scans the upstream’s reply for that exact card number and puts the token back before the response reaches you. Many processors echo the card number in their confirmation object, in a Location redirect or in a plain-text receipt — without this, the number they echoed would land in your backend, your logs and your database, which is precisely what tokenization exists to prevent. What is replaced, and what is not:
  • Only the values this request opened, matched exactly. A different card number the processor happens to mention is your own data and is left alone; this is not a general card scan of your responses.
  • Only pan vaults. A value from a generic, ssn or email vault echoed back is untouched.
  • However the processor spelled it. 4111111111111111, 4111 1111 1111 1111, 4111-1111-1111-1111 and the form-encoded 4111+1111+1111+1111 / 4111%201111%201111%201111 all match.
  • Body and headers. JSON fields (at any depth, including object keys and values sent as bare JSON numbers), form-encoded values, text bodies, and header values such as Location. A header value is replaced, never stripped, so a 3-D Secure redirect keeps working.
Two consequences to plan for:
  • A rewritten body is returned uncompressed. KnoxCall sends Accept-Encoding: identity upstream on any request that detokenizes a pan token, because a compressed reply cannot be scanned. If the upstream ignores that and compresses anyway, the response is withheld with 502 response_not_retokenizable rather than forwarded unscanned. The same answer is given for a binary body.
  • Streaming is refused for these requests. A card number can straddle any chunk boundary the upstream picks, so a streamed reply cannot be re-tokenized honestly. A request that both detokenizes a pan token and asks for streaming (Accept: text/event-stream, or "stream": true in the body) is answered 409 pan_streaming_unsupported. Send the card request without streaming.
Every response KnoxCall re-tokenizes writes a vault.retokenize audit entry naming the vault and how many occurrences were replaced — never the number and never the token. A non-zero count is worth acting on: it tells you your processor is echoing card data back.

When a referenced token cannot be resolved

A {{ token: … }} that KnoxCall cannot exchange for its value stops the request before the upstream is called — nothing is sent with the hole unfilled. The status says whether retrying can help: Every 4xx above is final: the identical request is refused identically. The Routes surface answers the same conditions with its own relay_* codes — see why a Route field action refused your request.

Raw card numbers are refused on egress

KnoxCall refuses to forward a request whose body contains a payment card number in the clear, unless you have explicitly named the destination as one that may receive raw card data. This applies in both body modes, including X-Knox-Proxy-Mode: transparent, whose bytes are scanned as text.
The two ways forward.
  1. Tokenize the card and send the token — the intended path. Put the number in a vault whose token_format is pan and send {{ token: kcv_… }}. KnoxCall detokenizes it only toward that vault’s own allowed_destinations, so the number reaches your processor and nothing else. A value detokenized this way is never refused by this check.
  2. Allow the destination — if a host genuinely must receive the number itself, an owner or admin replaces the workspace’s raw-card egress allowlist:
    Entries are bare hostnames or *.suffix wildcards — no scheme, port, path or IP literal. Adding a host requires a fresh second factor; removing one does not, because shutting the door is the lever you want reachable during an incident. On a configured Route there is a narrower option — the route’s Security tab grants raw-card egress for that route in that environment alone, without opening the destination to /v1/proxy, workflows or webhooks. See Routes. Every change is audited as egress.pan_allowed_hosts_updated with the previous list, so it can be reverted from the trail alone. The list is empty by default, which is why the refusal is what you see first. (There is no dashboard screen for this yet — the API is the surface.) The matching GET returns the list plus refused_this_month, so you can see how often the refusal has fired without trawling the audit log.
What counts as a card number. 13–19 digits, optionally grouped with spaces or hyphens, that pass the Luhn checksum and begin with a recognised issuer prefix (Visa, Mastercard, Amex, Discover, Diners, JCB, UnionPay). An ordinary 16-digit order or reference number is not refused: both tests have to pass, and roughly nine in ten arbitrary 16-digit numbers fail the checksum alone. Where else it applies. The same refusal runs on configured Routes, the workflow HTTP Request step, the AI Gateway, and outbound webhook payloads — so there is no second door. Each writes an egress.raw_pan_refused audit entry naming the destination host and a masked sample (last four digits only); the number itself is never logged, never stored and never echoed back to you.

Limits and gates

Audit and observability

Every invocation writes:
  • An api_requests row with proxy_mode = 'ephemeral', status code, latency, upstream host, token count, and source IP. The body, headers, and token values are not stored.
  • An audit_log entry tagged ephemeral_proxy.invoke, carrying retokenized_occurrences — how many echoed card numbers were replaced with their token on the way back (0 on every request that opened none).
  • One vault.retokenize entry per vault whose card number the upstream echoed, with the occurrence count. The vault’s id is recorded; the number and the token never are.
  • A Fleet Graph signal carrying (method, upstream_host, status_code, latency_ms) — never the body, never the token strings, never the customer’s path.
You can query the audit chain via GET /v1/audit-logs?action=ephemeral_proxy.invoke (Audit Logs).

Comparison: Routes vs Ephemeral Proxy

Next steps