Policies
One cluster-scoped resource decides what may run. The console edits it live; kubectl apply edits it from a repository. They are the same object, so GitOps and the UI never fight over a second source of truth.
Where policy lives
Policy is an ImageSecurityPolicy, a cluster-scoped custom resource in the
attestkeep.com API group. The chart installs one named default. Look at it
the way you would look at anything else in the cluster:
kubectl get imagesecuritypolicies
kubectl describe imagesecuritypolicy default
kubectl get imagesecuritypolicy default -o yaml
A policy edit takes effect on the next admission decision. There is no redeploy, no restart, and nothing to reload.
How a namespace gets its policy
Two rules, checked in order:
- A policy whose
spec.targetNamespaceslists the namespace wins. - Otherwise the policy with no
targetNamespacesapplies — that is the global default.
So the usual arrangement is one global policy, plus a stricter one naming the namespaces that have earned it. A namespace listed nowhere, in a cluster whose only policies are all targeted, is not covered — keep one global policy unless you mean that.
Example: the shipped default, annotated
This is the policy the chart installs, with the reasoning attached. It admits workloads whose images are not yet scanned, queues the scan, and refuses once findings cross the thresholds — the adoption-friendly posture.
apiVersion: attestkeep.com/v1alpha1
kind: ImageSecurityPolicy
metadata:
name: default
spec:
# Enforce refuses; Audit only records what it would have refused.
enforcement: Enforce
# AllowAndScan admits an unknown digest and queues the scan.
# DenyUntilScanned refuses anything without a scan record.
admissionTiming: AllowAndScan
# A mutable tag can change what runs without anyone deploying.
blockLatestTag: true
# AllowTags | PreferDigest | Required — whether images must be pinned
# by digest. Required is the strongest claim and the strictest rollout.
digestEnforcement: AllowTags
thresholds:
blockCritical: true # any critical finding refuses the image
blockHigh: true # any high finding refuses the image
maxMedium: 0 # 0 means unlimited here; set a ceiling to refuse past it
maxLow: 0
# How long a finding may stay open before the report calls it overdue.
slaWindows:
critical: 24h
high: 168h
medium: 720h
low: 2160h
# Supply-chain requirements, all off by default: turn them on once your
# build pipeline actually produces signatures and attestations.
requireSignature: false
requireSBOM: false
requireVulnAttestation: false
attestationMaxAge: 168h
# Accepted-risk decisions: how long a triage may stand, and whether it
# must say why.
triageDefaults:
maxDuration: 2160h
requireJustification: true
kubectl apply -f policy-default.yaml
Example: audit-only adoption
The first week on an existing cluster. Nothing is refused; every decision that would have refused is recorded, so you learn what enforcement will cost before paying it.
apiVersion: attestkeep.com/v1alpha1
kind: ImageSecurityPolicy
metadata:
name: default
spec:
enforcement: Audit
admissionTiming: AllowAndScan
blockLatestTag: true
digestEnforcement: AllowTags
thresholds:
blockCritical: true
blockHigh: true
Example: a production namespace that has earned strictness
The global default stays adoption-friendly; prod gets the hard gate. Unknown
images are refused until scanned, images must be pinned by digest, must come from your
registry, must be signed, and workload hardening findings refuse rather than warn.
apiVersion: attestkeep.com/v1alpha1
kind: ImageSecurityPolicy
metadata:
name: prod-hard-gate
spec:
targetNamespaces: ["prod", "payments"]
enforcement: Enforce
admissionTiming: DenyUntilScanned
blockLatestTag: true
digestEnforcement: Required
# Anything not from these hosts is refused outright.
allowedRegistries:
- registry.example.com
- ghcr.io/your-org
thresholds:
blockCritical: true
blockHigh: true
requireSignature: true
requireSBOM: true
requireVulnAttestation: true
attestationMaxAge: 168h
# off | warn | enforce. enforce refuses privileged containers, hostPath
# mounts and their relatives; exempt lists workloads allowed anyway.
workloadHardening:
mode: enforce
minSeverity: high
exempt: []
Pair DenyUntilScanned with webhook.failurePolicy: Fail in the chart
values if the gate must hold even while the operator is down — see
Admission for what that trade costs.
Either way the first scan of a new digest runs in the background. On a fresh cluster it can take a few minutes — the scanner pulls the image and the vulnerability database is cold — and until it finishes the image shows as Unknown on the Images page with the reason "admitted before first scan". Every later admission of that digest decides on the finished report. Both timings are available in every edition, Community included; Rescan on the Images page runs a scan on demand.
Example: break-glass
Production is down, the fix is an image the policy refuses, and the incident channel has agreed to let it through. Break-glass admits the named images, records every use of the exception, and expires on its own — an exception that cannot outlive the incident.
apiVersion: attestkeep.com/v1alpha1
kind: ImageSecurityPolicy
metadata:
name: default
spec:
enforcement: Enforce
admissionTiming: AllowAndScan
blockLatestTag: true
thresholds:
blockCritical: true
blockHigh: true
breakGlass:
enabled: true
reason: "INC-2417: rollback image carries a known high finding"
approvedBy: "on-call, incident commander"
auditRef: "INC-2417"
expiresAt: "2026-09-01T06:00:00Z"
affectedImages:
- registry.example.com/payments/api:1.41.2
Every admission that passed only because of break-glass is marked as such in the audit trail and in compliance reports — the exception is evidence too, which is the point.
Example: runtime reconciliation
The admission ledger records what crossed the boundary under the control. It says
nothing about what is running that never crossed under it: a workload that predates the
install, one that entered during an availability window, or one that slipped through
under webhook.failurePolicy: Ignore keeps running long after the window
closed, and no admission event names it.
The reconciliation sweep closes that gap. On a schedule
(reconcile.interval, default hourly) the operator reads every running
container's observed imageID — not the spec's image reference,
which under a mutable tag can still match its original admission event while the bytes
underneath have changed — and checks each digest against the ledger. A running digest
with no allowed admission event behind it is exactly the set that entered outside the
control. What happens next is this policy's choice:
apiVersion: attestkeep.com/v1alpha1
kind: ImageSecurityPolicy
metadata:
name: default
spec:
enforcement: Enforce
admissionTiming: AllowAndScan
# audit — record the finding in the sweep history and the evidence document.
# notify — also announce a newly seen unverified image on the notification
# channels (announced once per digest, not once per sweep).
# enforce — also stop the pod, so its controller recreates it through the
# webhook. Re-admission, not punishment: the recreated pod either
# gets a recorded denial or a recorded admission, and both are more
# than it had. A few pods per sweep, so a cluster full of
# pre-existing workloads drains through the gate gradually. A pod
# with no controller is reported, never stopped — see below.
runtimeReconciliation: audit
Enforcement is armed with two keys. The policy above chooses the mode; the chart must
also grant the permission it needs — rbac.allowEnforce: true — or the sweep
logs the missing grant and stops nothing. Stopping means asking for eviction,
the subresource where the API server applies PodDisruptionBudgets, so a service whose
every replica is unverified drains at the pace its budget allows rather than going down
at once. And a digest whose pod was already stopped once is left alone for a day: if its
re-admission produced no allowed event, stopping it again every sweep would be a crash
loop administered by the security tool — it stays reported, and a human owns the next
move.
Enforcement only applies where re-admission is possible. Stopping a pod
sends it back through the webhook only because something recreates it. A pod applied by
hand has no controller, so evicting it would answer the finding by deleting the workload
the finding was about — no denial recorded, no admission recorded, and the evidence left
showing an absence where an unverified workload used to be. A static pod fails the same
requirement from the other end: the kubelet rebuilds it from a file on the node without
going near admission, so stopping it would loop forever without producing an event.
Both are reported and left running, marked unmanaged in the workload list,
because rescheduling a workload that nothing will bring back is a decision that belongs
to a person and not to a sweep.
Findings land in the evidence document's population section either way,
alongside the sweep's own honesty markers: containers the webhook would not have
intercepted are counted apart as out-of-scope, side-loaded images that carry no registry
digest are reported as unverifiable rather than folded into either verdict, and a
truncated workload list says so. During break-glass, enforce quietly steps
down to notify — a pod stopped mid-incident would be readmitted through the
bypass, which would launder it into an allowed event instead of judging it.
Base OS support and known-exploited CVEs
Two gates that decide on something other than a severity count. Both take ignore, warn or block, and both default to warn, so a policy written before they existed keeps admitting exactly what it admitted before and starts reporting why it should not.
apiVersion: attestkeep.com/v1alpha1
kind: ImageSecurityPolicy
metadata:
name: default
spec:
# The image's base OS is past its vendor's support window: no security fixes
# will ever be published for that release, so its findings can only grow.
# warn (default) admits and says so; block refuses whatever the counts are;
# ignore says nothing.
endOfLifeOS: warn
# The image carries a CVE on CISA's Known Exploited Vulnerabilities
# catalogue — not a prediction of what an attacker could do, a record of
# what attackers have done. warn (default) admits and names the CVEs;
# block refuses; ignore says nothing.
knownExploited: warn
The base OS and its support state are shown on each image's detail panel. The KEV catalogue is kept as a local copy, refreshed every six hours, and every finding whose CVE appears on it is marked; the vulnerability and image pages carry a KEV badge with the catalogue's dates and can be filtered down to it, the dashboard shows which catalogue version is in force, and evidence packages record that version alongside the vulnerability database version. Triaged CVEs are excluded from this gate, the same way they are excluded from the severity thresholds, and a refusal names the CVEs rather than a count, so a refused deployment says what to fix.
A cluster with no route to the internet points kev.source at a mirrored copy of the catalogue, or sets it empty to keep the copy it already has — see kev.source.
Build provenance
Not "somebody signed these bytes" but "this build system produced them from this source", which is the question a compromised release pipeline actually turns on.
apiVersion: attestkeep.com/v1alpha1
kind: ImageSecurityPolicy
metadata:
name: prod-hard-gate
spec:
provenance:
# 0 (the default, and what an absent block means) switches the gate off.
# Above zero the image must carry a verified SLSA provenance attestation
# naming a builder listed below, credited with at least this level.
minLevel: 2
trustedBuilders:
# The id is what the statement claims. A trailing "*" matches by
# prefix; every other id matches exactly. An empty list with
# minLevel above zero admits nothing.
- id: https://github.com/acme/build/.github/workflows/release.yml@*
# What this operator asserts about that builder, not something read
# from the predicate: SLSA records no level inside a provenance
# statement, because a level describes how the builder is run and
# isolated and no statement can vouch for that about itself.
level: 3
# Who made the claim, read from the signing certificate. Unset, it
# must equal the builder id — the shape a reusable workflow produces.
signer: https://github.com/acme/build/.github/workflows/release.yml@*
# Unset accepts any issuer. Worth naming on a cluster that only ever
# builds in one place.
issuer: https://token.actions.githubusercontent.com
An image whose verified SLSA provenance — v1 or v0.2 — does not name a trusted builder, or whose signing certificate identity or issuer is not the one the entry accepts, or whose builder is credited below minLevel, is refused, and the reason names the builder, the signer or the level. Both identities are read from the certificate the registry stores beside the attestation, which the scanner fetches itself: an image whose certificate cannot be reached or parsed is refused rather than admitted on the predicate's word alone.
This is the only check that binds to who. requireSBOM and requireVulnAttestation verify that somebody signed the attestation; the provenance gate verifies which identity did. The predicate, builder, signer and issuer appear on the scan row and on the console's image detail, and evidence packages carry provenance-verified facts.
Producer statements, and where the pod runs
Two blocks large enough to have their own pages:
vex— a signed OpenVEX statement from a producer the policy trusts can take a finding out of the counts admission decides on.modeisignoreby default, which verifies, records and shows every trusted statement while moving no verdict.deploymentContext— judge an image more strictly according to where the pod is going to run: exposed, privileged, root, host namespaces, host filesystem, added capabilities.modeisIgnoreby default, which records the signals on every ledger row and changes no decision. Escalation can only tighten a policy, never loosen it.
Both are worth running in their default mode first, and the what-if replay is how you find out what switching would cost before you switch it.
Field reference
| Field | Values | What it decides |
|---|---|---|
enforcement | Enforce | Audit | Refuse, or only record what would have been refused. |
admissionTiming | AllowAndScan | DenyUntilScanned | What happens to an image with no scan record yet. |
blockLatestTag | bool | Refuse the one tag that changes underneath you. |
digestEnforcement | AllowTags | PreferDigest | Required | Whether images must be pinned by digest. |
allowedRegistries | list of hosts | Empty allows any registry; non-empty refuses all others. |
targetNamespaces | list | Empty makes this the global policy; non-empty scopes it to those namespaces, overriding the global one there. |
thresholds | — | blockCritical/blockHigh refuse on any such finding; maxMedium/maxLow set ceilings (0 = unlimited). |
slaWindows | durations | How long a finding of each severity may stay open before reports call it overdue. |
requireSignature | bool | Refuse images without a verified cosign signature. |
requireSBOM | bool | Refuse images without an SBOM attestation. |
requireVulnAttestation | bool | Refuse images without a vulnerability-scan attestation. |
attestationMaxAge | duration | An attestation older than this no longer counts. |
workloadHardening | mode, minSeverity, exempt | Whether privileged containers, hostPath mounts and similar refuse (enforce), warn, or pass; exempt names workloads allowed anyway. |
endOfLifeOS | ignore | warn | block | What happens to an image whose base OS is past its vendor's support window. Default warn. |
knownExploited | ignore | warn | block | What happens to an image carrying a CVE on CISA's Known Exploited Vulnerabilities catalogue. Default warn. |
provenance | minLevel 0–3, trustedBuilders | Refuse an image whose verified SLSA provenance does not name a trusted builder, is not signed by the identity that entry accepts, or is credited below minLevel. Default minLevel: 0, the gate off. |
vex | mode ignore | apply, trustedIssuers | Whether a trusted producer's signed not_affected or fixed statement takes a finding out of the counts. Default ignore — recorded and shown, nothing suppressed. See producer statements. |
deploymentContext | mode Ignore | Escalate, signals | Judge a pod carrying a selected signal more strictly. Default Ignore — signals recorded, no decision changed; an empty signals list under Escalate means all six. See deployment context. |
runtimeReconciliation | audit | notify | enforce | What the periodic sweep does about a running image with no admission event behind it: record it, announce it, or stop the pod so it re-enters through the webhook. |
breakGlass | — | A named, approved, expiring exception; every use is recorded. |
triageDefaults | maxDuration, requireJustification | How long an accepted risk may stand, and whether it must say why. |
storageUnreachablePolicy | Allow | Deny | The admission answer while the database is unreachable. |