Skip to main content

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

  1. Why does continuous authorization log allows and not only denials? Give a concrete example of an allow you would need in an audit.
  2. The mask rule reads input.input.subject.ssn, not input.subject.ssn. Explain the extra input level.
  3. Replay depends on masking leaving certain input fields intact. Which field in the lab would break a replay if you masked it, and why?