Most masking implementations fail open. You list the fields you want masked; everything else passes through unchanged. This is the intuitive design: you know your data model, you mark what’s sensitive, and you move on. The problem is that the default behavior is incorrect. Any field you didn’t list, any field added by a third-party API without warning, any field your schema doesn’t document: all of them pass through unmasked until someone notices.
The alternative is a fail-closed posture: broad rules that cover field families rather than individual names, value-shape matching that catches sensitive patterns wherever they appear, and a conservative default for anything that doesn’t fit a clearly safe category. This guide covers what that looks like in Stubsmith’s masking configuration and why the distinction matters.
Fail-open vs. fail-closed
In a fail-open model, you maintain a blocklist of fields to mask. Unknown fields pass through. A new contactEmail field added by an upstream API during a quarterly release, and not on your blocklist, enters your fixtures unmasked. A billing_token that appears in a webhook payload you hadn’t seen before passes through. The model only protects what you’ve already thought of.
In a fail-closed model, the default is protective. You write rules that cover entire families of likely-sensitive fields. You use value-shape patterns to catch sensitive content by what it looks like rather than where it lives. You treat free-text fields as sensitive unless there is a specific reason not to. When an unknown field appears, the question is “is this safe to capture unmasked?” rather than “is this on the blocklist?”
Both models produce identical results for fields you’ve explicitly listed. They diverge on the fields you haven’t thought of, which is exactly where the meaningful failures happen.
Why blocklists accumulate gaps over time
An upstream API ships a new response field: referral_email. Your field_masks list includes email. It does not include referral_email, because you wrote the rules when that field didn’t exist. The new field goes into your fixtures unmasked.
A support integration starts including customer_notes in its webhook payload. You have notes in your list. You don’t have customer_notes. Real customer text, including names, addresses, and complaint details, accumulates in your fixture store.
A payment provider begins returning a recipient_iban field alongside the existing iban field you already mask. You miss it for three weeks until someone reviews a fixture manually.
These failures are the ordinary consequence of a blocklist that grows by exception. The blocklist is always exactly as complete as your attention at the time you last reviewed it.
Field family patterns
Rather than listing each field name individually, think in families and enumerate the plausible variants of each family. Stubsmith’s field_masks configuration takes an array of exact field name strings rather than regex patterns, so you need multiple entries to cover a family, but you can be systematic about it.
Common families and their variants:
- Email:
email,e_mail,emailAddress,email_address,contactEmail,userEmail,billingEmail,replyTo,reply_to - Phone:
phone,phoneNumber,phone_number,mobile,cell,tel,fax - Name:
name,firstName,first_name,lastName,last_name,fullName,full_name,displayName,display_name - Authentication material:
password,passwd,token,access_token,refresh_token,id_token,api_key,apiKey,secret,credential,auth,authorization - Identity and government numbers:
ssn,sin,dob,date_of_birth,passport,passport_number,license,tax_id,national_id - Payment data:
credit_card,card_number,cardNumber,cvv,cvc,iban,account_number,accountNumber,routing_number,bic,swift - Free-text fields:
notes,description,comment,memo,message,reason,instructions,remarks
Adding all plausible variants costs nothing at runtime. Missing a variant costs you a gap in your masking coverage.
{
"field_masks": [
"email", "e_mail", "emailAddress", "email_address",
"contactEmail", "userEmail", "billingEmail", "replyTo", "reply_to",
"phone", "phoneNumber", "phone_number", "mobile", "tel",
"name", "firstName", "first_name", "lastName", "last_name",
"password", "passwd", "token", "access_token", "refresh_token",
"api_key", "apiKey", "secret", "credential", "auth", "authorization",
"ssn", "dob", "date_of_birth", "passport",
"credit_card", "card_number", "cvv", "iban", "account_number",
"notes", "description", "comment", "memo", "message", "reason"
],
"regex_masks": [
{
"pattern": "\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b",
"replace": "user@example.com"
},
{
"pattern": "\\b[A-Z]{2}\\d{2}[A-Z0-9]{4}\\d{7}([A-Z0-9]?){0,16}\\b",
"replace": "XX00XXXX0000000"
},
{
"pattern": "\\b\\d{16}\\b",
"replace": "0000000000000000"
},
{
"pattern": "Bearer\\s+[A-Za-z0-9\\-\\._~\\+\\/=]{8,}",
"replace": "Bearer <masked>"
},
{
"pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b",
"replace": "000-00-0000"
}
]
}
The regex_masks patterns use RE2 syntax and match against field values regardless of which field they’re in. This is the second layer of the fail-closed approach: catch sensitive content by what it looks like, not only by where it is named.
Value-shape matching as the second layer
Some sensitive values have recognizable shapes wherever they appear. Email addresses, IBANs, card numbers, US SSNs, and bearer tokens all have structure you can express as a pattern. Adding regex_masks for these shapes means a misnamed field, an unexpected field from a third party, or an email address mentioned in free text is caught even when the field name doesn’t trigger a match.
Consider a payload where an email address appears inside a free-text notes field. A field name rule won’t reach it, but the value-shape rule does:
Before masking (what the middleware receives at the edge):
{
"order_id": "ORD-8821",
"status": "pending",
"notes": "Customer john.smith@example.com called to update the shipping address.",
"amount": 149.00,
"is_flagged": false
}
After masking (what Stubsmith persists):
{
"order_id": "ORD-8821",
"status": "pending",
"notes": "Customer user@example.com called to update the shipping address.",
"amount": 0,
"is_flagged": false
}
The notes field was in field_masks in the configuration above, so the entire value would be blanked. If it weren’t, the email regex pattern in regex_masks would still catch the email address within the string. The amount became 0 because shape-preserving masking converts all numbers to 0. The is_flagged boolean remains false. The structural shape of the payload is intact; a test that checks the type or presence of these fields continues to work. The value that would identify a person is gone.
The default for free-text fields
Fields typed as arbitrary strings (notes, description, comment, memo, reason, message) are where sensitive content most often appears in unstructured form. Regex patterns catch some of it (emails, phone numbers in common formats, IBANs), but natural language contains PII that resists pattern matching: “Please deliver to Jane at 14 Elm Street” is sensitive, and no regex catches it reliably.
The fail-closed approach to free-text is to add these field names to field_masks explicitly. The full string value is replaced by a placeholder. The field remains present and correctly typed, still a string, but its content is gone.
Teams sometimes resist this because they have tests that assert on the content of a notes field. That assertion is usually a mistake in the test, not a reason to leave the field unmasked. If your test logic depends on specific text in a free-text field, that dependency should be expressed in the test itself with a fixture you control, not through a captured production value.
Testing rules before enabling capture
Before pointing any real traffic at the ingest service, verify your rules against representative sample payloads. Use dummy data, never real user data, to compose a realistic-looking request/response pair for each distinct API shape you capture. Run those samples through the Stubsmith dashboard’s rules preview. Check that every field that should be masked shows its placeholder and that fields your tests rely on remain structurally present.
Do this for each distinct response shape your service handles. A payment webhook payload has a different field set than a user profile response; both need their own coverage review. Gaps are cheap to find and fix at this stage. They are expensive to find after real traffic has been captured.
How fail-closed fingerprints fit
Stubsmith’s ingest service computes structural fingerprints from the masked payload to deduplicate captures. When the service cannot safely process a capture, because a field value doesn’t match expected patterns and cannot be safely handled, it drops the sample rather than persisting it. The fail-closed behavior extends to the storage layer: uncertain samples are discarded, not accumulated.
The consequence is that some traffic shapes may not appear in your fixtures if the ingest service cannot process them safely. That is the right trade-off. A missing fixture is a gap you can fill deliberately by adjusting your rules and re-running. A fixture containing unmasked personal data is a compliance incident you discover much later, from a worse position.
A checklist before enabling capture in production
- Every field name in your API’s response schemas is covered by a
field_masksentry or explicitly reviewed and confirmed non-sensitive. - Common variants of each sensitive field name are included, not just the canonical form (
email,emailAddress,contactEmail, etc.). regex_maskscovers email addresses, payment card numbers, IBANs, bearer tokens, and any other value-shaped sensitive patterns your APIs return.- All free-text fields (
notes,description,comment,message,reason) are infield_masks. - You have run representative sample payloads through the rules preview and confirmed the masked output looks correct.
- You have reviewed what remains after masking as a set, not field by field, to check whether the combination of unmasked fields creates re-identification risk.
Rules are cheap to add and free to update. The cost of a gap in your rules is not.