Closing the loop: decision logs, masking, and replay
Objective
Explain how decision logs, field masking, and Writ replay close the loop from enforcement back to audit, and verify that masking removes the right fields while preserving the evidence an auditor needs.
Concept
Define, Enforce, Audit is a loop, not a line. The PDP enforces a decision; the audit stage records it, protects it, and feeds it back into the next change. Three mechanisms do that.
Decision logs
Every decision the PDP makes is emitted as a decision-log event: the
input, the result, and metadata. Continuous authorization depends on logging
every decision, allows included, not just denials, because an allow is exactly
what you need to prove access was authorized. In the lab, the structured
decision object ({"allow": ..., "policy_id": "customer-records-v1"}) is the
result worth logging.
Masking sensitive fields
The request that produced a decision often carries data that must never land in
a log: in the lab, the subject SSN and the resource card number. OPA evaluates a
rule at data.system.log.mask over each event before shipping it and removes
every JSON pointer the rule returns. Enterprise OPA exposes the same idea through
its decision_logs.mask configuration as a managed pipeline stage.
The lab's mask.rego (package system.log) is already complete:
package system.log
mask contains "/input/subject/ssn" if {
input.input.subject.ssn
}
mask contains "/input/resource/card_number" if {
input.input.resource.card_number
}
The event is the input to this policy, so the original decision input is at
input.input. The pointers select the SSN and card number for removal. The org,
the classification, and the result are deliberately left in, because those are
the evidence an auditor and a replay need. Masking that also destroys the
evidence is not masking, it is data loss.
Replay
A decision log records what happened. Replay re-runs those recorded inputs
against a candidate bundle and diffs the decisions: which requests that were
allowed would now be denied, and vice versa. That is a regression test against
real traffic, run before a promotion, so the blast radius of a policy change is
known before users hit it. Replay is why masking must preserve input fields
the policy reads: a masked-away field the policy depends on would make the
replay lie.
Hands-on lab
The masking tests in ../lab/policy_test.rego already
pass, but read them, because they are the model for the hidden certification
suite. They build a log_event fixture (input plus result), collect
data.system.log.mask over it, and then use json.remove to reproduce exactly
what OPA ships after redaction. Four checks:
- the mask selects the SSN and card-number pointers,
- the mask does not select the org, the classification, or the result,
- after redaction the SSN and card number are actually gone, and
- after redaction the org, classification, and result survive.
Confirm they pass on their own:
git clone https://github.com/EnforceAuth/university-labs.git
cd university-labs
opa test courses/capstone-four-product-fabric -v
Then make one deliberate mistake to feel the seam: temporarily add
mask contains "/input/resource/classification" to mask.rego and re-run. The
"preserves evidence" test fails, because you just masked away a field the audit
trail (and any replay that reads classification) needs. Remove it and the suite
is green again. Masking is a scalpel, not a firehose.
Check for understanding
- Why does continuous authorization log allows and not only denials? Give a concrete example of an allow you would need in an audit.
- The mask rule reads
input.input.subject.ssn, notinput.subject.ssn. Explain the extrainputlevel. - Replay depends on masking leaving certain
inputfields intact. Which field in the lab would break a replay if you masked it, and why?