Secrets and redaction

Secrets and redaction

apifae record proxies your traffic, and traffic carries credentials. This page says exactly what ends up on disk, what does not, and how to change it.

What is redacted

Request header values, unless the header is one of these:

accept, accept-encoding, accept-language, content-type, content-length, user-agent, referer, origin, and anything starting sec-fetch- or sec-ch-ua.

Everything else — authorization, cookie, x-api-key, and any custom header your API uses — is written as <redacted>.

This is an allow-list on purpose. A deny-list only protects against headers somebody thought of in advance, and a header named X-Acme-Session is exactly the one nobody would have listed. Nothing reads these values back, so keeping fewer of them costs nothing.

Request query values, when the parameter name contains any of api_key, apikey, access_token, token, secret, signature, sig, password or auth. Matched case-insensitively, as a substring, so apiKey and user_password both match.

Query uses a deny-list rather than an allow-list because a query value usually carries meaning you want when reading a capture — ?status=active&page=2 is the request, not noise around it.

Bodies

When record --convert writes a mock, it walks the response body and replaces string values whose key looks like a credential — a key containing token, secret, password, apikey, credential, authorization or privatekey, matched at any depth, case-insensitively, ignoring - and _.

- path: /login
  method: POST
  responses:
    - status: 200
      body:
        access_token: <redacted>
        refresh_token: <redacted>
        expires_in: 3600
        user:
          email: a@b.c

Only strings are replaced. expires_in: 3600 and token_count: 5 keep their values and their types — rewriting a number to "<redacted>" would hand a consumer of the mock something it never expected.

The cost of substring matching is over-redaction: a field called tokenizer is replaced too. Given the alternative is a token in a committed file, that is the trade this makes.

Add your own keys with convert.redact_body_keys, or turn the whole thing off with convert.redact_bodies: false or --no-redact.

What is not

Raw and binary bodies. A body stored as raw_body (text, XML, CSV) or raw_body_base64 (images, gzip, protobuf) is never scanned. There are no keys to match on, so the only option would be a regex over arbitrary text — the kind of check that silently matches nothing and reports success.

record --convert names them instead:

  Warning: 2 responses have non-JSON bodies (raw_body / raw_body_base64).
    These are NOT scanned for secrets:
      POST /oauth/token
      GET  /export.csv
    Review before committing.

Recorded sessions. .apifae/recordings/ keeps the real response bodies on purpose: a capture is meant to be a faithful record of what the API returned. It is gitignored, and body redaction happens on the way into mocks/, which is the directory you commit.

Traffic in flight. Redaction happens on the way to disk. Your API receives exactly the request your client sent, header for header. Recording never changes what the target sees.

What a redacted session looks like

request:
  method: GET
  path: /orders
  headers:
    user-agent: curl/8.7.1
    authorization: <redacted>
    accept: application/json
  query:
    api_key: <redacted>
    status: active

The header names survive. You can see the request was authenticated and which header carried it; you cannot recover the credential.

Each session's metadata.yaml records whether it was redacted:

name: demo
request_count: 1
unique_endpoints: 1
bodies_dropped: 0
redacted: true

so a session you find later is not ambiguous.

Turning it off

For one run:

$ apifae record https://api.example.com --no-redact

For a workspace, in apifae.yaml:

record:
  redact: false

The flag wins over the config. Either way record prints a warning when redaction is off — at the start of the session and again at the end, because the first one has scrolled away by then.

Keeping specific headers

Rather than turning redaction off wholesale, opt individual headers back in:

record:
  keep_headers:
    - x-request-id
    - x-trace-id

authorization, cookie and proxy-authorization are never kept, whatever this list says. If you genuinely need one of those on disk, use --no-redact and be deliberate about it.

Redacting more query parameters

record:
  redact_query:
    - tenant_ref

Names here extend the built-in list and are matched the same way.

Recordings and git

apifae init adds .apifae/ to your .gitignore. If your workspace predates that, add it yourself:

.apifae/

A redacted session is still a record of every path your client called and every body your API returned. That is usually fine to keep locally and rarely something to publish.

This manual is generated from the repository. Every command and flag in it is checked against the binary; the marked transcripts are executed.

Last updated .