Attestkeep docs

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:

  1. A policy whose spec.targetNamespaces lists the namespace wins.
  2. Otherwise the policy with no targetNamespaces applies — 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:

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

FieldValuesWhat it decides
enforcementEnforce | AuditRefuse, or only record what would have been refused.
admissionTimingAllowAndScan | DenyUntilScannedWhat happens to an image with no scan record yet.
blockLatestTagboolRefuse the one tag that changes underneath you.
digestEnforcementAllowTags | PreferDigest | RequiredWhether images must be pinned by digest.
allowedRegistrieslist of hostsEmpty allows any registry; non-empty refuses all others.
targetNamespaceslistEmpty 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).
slaWindowsdurationsHow long a finding of each severity may stay open before reports call it overdue.
requireSignatureboolRefuse images without a verified cosign signature.
requireSBOMboolRefuse images without an SBOM attestation.
requireVulnAttestationboolRefuse images without a vulnerability-scan attestation.
attestationMaxAgedurationAn attestation older than this no longer counts.
workloadHardeningmode, minSeverity, exemptWhether privileged containers, hostPath mounts and similar refuse (enforce), warn, or pass; exempt names workloads allowed anyway.
endOfLifeOSignore | warn | blockWhat happens to an image whose base OS is past its vendor's support window. Default warn.
knownExploitedignore | warn | blockWhat happens to an image carrying a CVE on CISA's Known Exploited Vulnerabilities catalogue. Default warn.
provenanceminLevel 0–3, trustedBuildersRefuse 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.
vexmode ignore | apply, trustedIssuersWhether 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.
deploymentContextmode Ignore | Escalate, signalsJudge 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.
runtimeReconciliationaudit | notify | enforceWhat 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.
triageDefaultsmaxDuration, requireJustificationHow long an accepted risk may stand, and whether it must say why.
storageUnreachablePolicyAllow | DenyThe admission answer while the database is unreachable.