Try a policy before enforcing it
A policy is easy to read and hard to predict. Whether Enforce would refuse anything in your cluster depends on your images, your scan results and your triage decisions — not on the policy text.
That is why installations sit in Audit for months: the operator cannot see what switching would cost, so they never switch. The replay answers it from the cluster's own records. Every admission review is already in the ledger; a draft policy is decided against those rows again, by the same code the admission webhook runs, and the differences are reported before anything is saved.
Nothing is written. No pod is affected, no scan is queued, no registry is contacted, and the licence meter is not touched. The one trace it leaves is an audit-log entry (policy_whatif, "replayed a draft policy") and a counter, attestkeep_policy_whatif_runs_total.
In the console
Policy editor → What if, beside Apply to cluster. Pick a period — 7, 30 or 90 days — and press it. The button is disabled while the form has errors: the draft goes through the same validation a save does, so a draft that cannot be tried is a draft that cannot be applied.
The panel reports three figures — unchanged, would be denied, would be allowed — the gate behind each difference, and up to two hundred of the changed admissions, most recent first.
The request
POST /api/v1/policies/whatif, session-authenticated. It needs the same authority as reading a policy, not writing one, because it writes nothing.
{
"policy": { "...": "the same body POST/PUT /api/v1/policies accept" },
"days": 30
}
days is 1 to 90 and defaults to 30. An invalid draft is refused with 400 and the exact message Apply would have given.
What the answer contains
| Field | What it is |
|---|---|
evaluated | Ledger rows the draft was decided against. |
distinct_images | How many distinct digests those rows cover. |
break_glass_skipped | Bypassed admissions, counted apart and not replayed: a bypass decided them, not a policy. |
truncated | The row cap — 5000, newest first — was reached, so the period is only partly covered. |
unchanged / would_deny / would_allow | The three figures the panel shows. |
would_deny_by_reason | Keyed by the labels attestkeep_admission_denials_total uses — threshold, known_exploited, provenance, registry, tag, digest, signature, attestation, unscanned, end_of_life, quota, context. |
would_allow_by_reason | Keyed by the original denial's label, recovered from the recorded reason. other when it cannot be recovered — the ledger stores the sentence a deployer was shown, not the label the metric carried. |
samples | Only the changed rows, most recent first, at most 200. The reason is the draft's own sentence. |
context | What the draft's deploymentContext gate would touch: its effective mode, how many replayed rows carried each of the six signals (every signal is a key, even at zero), and escalated_denials — refusals that happened only because of where the pod ran. rows_without_context counts rows recorded before migration 0020, which replay with no context at all; above zero it comes with a note saying so, because an empty array cannot tell "carried no signals" from "nobody was looking". |
What it covers
- Rows in the period whose namespace the draft governs. A draft with an empty
targetNamespacesis cluster-wide and covers every namespace. - Break-glass rows are excluded and counted in
break_glass_skipped. - Pod-level workload-hardening denials are excluded: they are recorded with no image, and the replay decides images. Reporting them as "unchanged" would be a claim it never checked.
- The newest 5000 rows.
The caveat that matters: the current scan record
Decisions are replayed against each image's scan record as it stands now, not as it stood at the time. That is what the note on every response says, and it is the one thing that can mislead.
The ledger records what was decided. The scan store records what is known today. An image scanned three times since that admission is judged on today's findings; a CVE published last week counts against a pod admitted last month. This is the right behaviour for the question being asked — "would this policy be safe to turn on" is about the images as they are — but it is not a reconstruction of history, and an evidence package is still the only thing that reports what was actually decided.
Two more things the replay does not reproduce:
- The licence meter. Metering is a licence fact rather than a policy one, so it is not re-run: replaying it would either double-count the period or make the answer depend on when the button was pressed. A row originally refused for the monthly image allowance therefore shows as "would be allowed", with
quotainwould_allow_by_reason. - Audit-mode masking. A denial recorded under an Audit policy sits in the ledger as allowed, with
audit mode:in front of its reason, and the replay compares it against the draft's own decision. That is deliberate, and it is the feature's whole point: a draft that differs from the running policy only inenforcementshows exactly the pods Enforce would have stopped.
An image with no scan record at all is decided the way admission decides a cold digest — by the draft's admissionTiming, or by the installation's webhook.coldImagePolicy when the draft leaves it empty — and lands under unscanned when that refuses it.
Why the answer can be trusted
The replay does not have its own copy of the rules. One function is the whole per-image admission decision — break-glass, the registry allow-list, :latest, digest enforcement, the meter, the cold-image timing, and the severity, known-exploited, provenance and VEX evaluation — and both the admission webhook and this endpoint call it. The preview and the gate cannot drift, because there is only one of them.