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 applied. No pod is affected, no scan is queued, no registry is contacted, and the licence meter is not touched. What it leaves behind is the run itself — kept for ninety days when a deployer or an administrator ran it, so it can be viewed, downloaded and compared later; a viewer's replay is answered but not kept — an audit-log entry (policy_whatif, "replayed a draft policy", naming the run) 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 stored run keeps up to two thousand changed rows and five hundred unscanned digests, and says when either cap cut (samples_capped, unscanned_capped).
Two shortcuts on the policies list: What if on an audit-mode policy opens the editor with the draft switched to enforce and replays the last thirty days immediately; on an enforcing policy it replays the policy as it stands, which surfaces admissions that would be refused today because their scan results have moved. Nothing is applied either way.
If some images in the period have no scan record, the panel says so — "N images have no scan record" — and offers Scan now (deployer and above), which queues scans for exactly those digests, five hundred per request. The replay is not re-run on its own; press What if again once the scans have finished. Until then those rows are decided the way admission treats a cold digest under the draft's admissionTiming.
History and downloads
Security › What-if history lists every run of every policy: when, by whom, the source policy, the period, how many admissions were reviewed and how many would be denied or allowed, and how many images were unscanned. It can be filtered by policy and period. Each run can be viewed again (the same panel, from the stored result), downloaded, or — for an administrator — opened in the editor with Apply this draft, which loads the stored draft without applying it — the diff is then shown against the policy as it is today.
The download is a projection, not evidence. The PDF carries that band on every page and is neither signed nor sealed; a run never enters an evidence package, because the package records what happened and a replay records what would have. The audit log records each download (policy_whatif_download).
GET /api/v1/policies/whatif/runs?policy=<name>&days=<7|30|90>&limit=&offset=
GET /api/v1/policies/whatif/runs/{id}
GET /api/v1/policies/whatif/runs/{id}/download?format=pdf|json
POST /api/v1/images/scan {"digests": ["sha256:…"]} # Scan now; at most 500
At most five hundred runs are kept; a new one past that removes the oldest, and runs older than ninety days are deleted by the operator's hourly sweep. One account may start at most ten replays a minute on each operator replica; the eleventh is answered with 429 and a Retry-After.
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" },
"source": "the policy the draft was edited from (must exist), empty for a new one",
"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 |
|---|---|
kept / run_id | Whether the run was stored and, if so, its id; the audit entry and the history page name it. A viewer's replay is not kept. |
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. The stored run keeps all of them. |
unscanned / unscanned_total | Distinct images in the period with no scan record, each with its digest — what Scan now queues — and their full count. |
notes | The three caveats, stated rather than implied: rows are decided against today's scan record, the licence meter is not replayed, and audit-mode denials are recorded as allows. |
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.