Skip to content

Type to search pages.

View .md

Verification

Masking is a claim. Verification is a check.

After the masking rules run, the engine scans the candidate for data that still looks like a person: addresses, card numbers, national identifiers, names in free text. If it finds anything, nothing is published. If it finds nothing, it signs a statement of what it scanned and what it found, and that statement is what makes the version branchable.

copy ──> mask ──> scan ──> attestation ──> golden
│
└── anything found: nothing is published

What the scan reads, and what it says it did not

Section titled “What the scan reads, and what it says it did not”

Strings, JSON and XML are read as they are. Arrays, enums and extension types are read through their text form. A bytea column is decoded as UTF-8 where it decodes, because a secret pasted into a binary column is text in a binary coat. Numbers, times, booleans and identifiers the database generates are not read, because their text form cannot carry a sentence somebody typed.

Anything else is listed as not readable by the scanner, with the type that made it so:

✓ clean 231 columns across 55 tables, 6111 rows sampled
! public.provider_keys.ciphertext: 4 of 4 sampled values are binary rather than text and could not be read (masked by its rule)
0 columns copied unchanged with no rule.

That line is the difference between “the scan found nothing” and “the scan found nothing in what it opened”. Until it existed the scan read six text types and nothing else, said clean, and a bytea holding a sealed private key was neither read, nor skipped, nor counted. af mask plan on the same database listed it as copied unchanged. Two instruments, one database, opposite answers, and the one that said clean was the one that gated publication.

The scan cannot fail every column it cannot read; an environment with no enum columns is no environment. It fails the narrow case where three facts line up: it cannot read the column, no masking rule covers it, and the name says what it holds.

AF-MSK-013 Verification could not read public.sso_connection_secrets.sp_private_key
(bytea), no masking rule covers it, and its name says it holds a secret.
Next: Give public.sso_connection_secrets.sp_private_key a rule in
masking.yaml, nullify or hash_hex, and refresh the golden.

The words are secret, key, token, private, ciphertext, password and credential, in the table name or the column name. A rule on the column, any rule, turns the failure into a note.

The detectors know Stripe’s object identifier families as well as its secret keys: cus_, sub_, in_, pm_, price_ and the rest, a prefix at the start of a token followed by a body of at least twelve letters and digits carrying a digit and a capital. A column of real customer ids trips it; a column masked with prefixed_id does not, because the masked body is lowercase hex. A Stripe identifier is not a secret, and it is exactly the kind of value the scan exists to catch: one that says which real customer a row belongs to, the same in every environment.

The scan does not know the rules. The command that runs it does, and it hands the scan the list of columns masking copied unchanged because no rule covered them. The scan carries that list into its report, so the attestation records the count and the names, af golden list shows the count beside verified, and inspect_goldens returns it. A verified golden with 145 of these is a different thing from one with none, and the listing used to say verified about both.

Because the rules are written by people. A column added last month has no rule, a rule can name the wrong column, and a notes field can hold an address somebody pasted into it. A masking pass that ran successfully proves the rules ran, not that the data is safe.

Verification is the part that can say no.

AF-MSK-001 The golden gv_20260826120000_a1b2c3d4 has no valid verification
attestation and cannot be branched.
Next: Run 'af golden verify gv_...'; a golden is branchable only once
verification has passed.

This is enforced in code rather than in a checklist. It is the product’s central promise: an environment cannot contain unmasked production data, because the only thing an environment can branch is a golden, and a golden is not a golden until the scan passed.

Where it is enforced differs by provider, and the difference is worth knowing. Neon, Supabase and Database Lab check the attestation at branch time and refuse with AF-MSK-001. The Docker provider, which is the default on a laptop, refuses earlier instead: a refresh whose verification fails never commits an image, so there is no unverified golden in existence to branch. That is the stronger place to refuse, and it is why the conformance behaviour named below passes for it.

It is not equivalent, and this page used to say it was. Two things follow from the Docker provider treating the existence of an image as the verification, and a reader relying on this page should have both:

  • A golden the provider lists is reported as verified because the image is there, not because anything re-read the attestation.
  • Re-running af golden verify on a published golden and having it FAIL does not stop that golden being branched again, because nothing marks it unverified afterwards. On the other three providers the next branch is refused.

The conformance suite every provider runs has a behaviour for exactly this, so a provider written outside this repository is held to it too.

AF-MSK-002 Verification found data matching card number in orders.notes.
Next: Add a masking rule for orders.notes and refresh the golden. The value
itself is never printed.

The value is never printed, and it is never written to a log, an artifact, or a CI annotation. A finding that quoted the data would publish it in the output of the job that caught it.

Add a rule and refresh:

masking.yaml
rules:
- table: orders
column: notes
transform: free_text
why: "customers paste anything into this field"

If the column genuinely holds no personal data and the detector is wrong, say so explicitly rather than deleting the check:

- table: orders
column: notes
transform: preserve
why: "internal fulfilment codes, never free text from a customer"

preserve is the exemption, and why is what makes it reviewable. An exemption with no sentence beside it is a decision nobody can check later, and af mask plan prints the sentence next to the column so it is read.

A signed statement: which version, which rules, which detectors ran, how many rows and columns were scanned, which columns the scanner could not read, which columns masking copied unchanged with no rule, and what was found. It is stored with the golden so anyone holding an environment can read what was checked without asking the engine.

With the Neon provider it lives in the branch itself:

SELECT version, rules_hash, created_at, attestation FROM _antifailure.golden;

It is signed so that an altered copy can be told from the original. af fidelity reads the stored attestation back in a process that did not sign it and checks the signature before repeating what it says. What that proves is that the document was not changed after it was signed. It does not prove who signed it, because the verifying key is generated for each signature and travels inside the document, so a machine that trusts an attestation is trusting whoever was able to write it.

Related: masking, goldens.