Skip to content

Vulnerability exceptions

A SecurityException can carry vulnerability entries alongside (or instead of) the posture entries described in Accepting risk with exceptions. Vulnerability entries are read by the in-cluster vulnerability scanner, kubevuln, and change how a CVE is reported on the workloads the exception matches.

They do not affect posture scans, and posture entries do not affect vulnerability scans.

Before you start

The in-cluster components read these resources only when the Helm value capabilities.riskAcceptance is set to enable:

helm upgrade kubescape kubescape/kubescape-operator \
  --reuse-values --set capabilities.riskAcceptance=enable

It defaults to disable. If it is off, matching resources are ignored: a warning is logged at startup, but nothing else reports it, so check this first if an exception appears to do nothing.

Example

Suppress CVE-2023-45853 on the nginx image, because the vulnerable code is not reachable in your deployment:

apiVersion: kubescape.io/v1beta1
kind: SecurityException
metadata:
  name: except-cve-2023-45853
  namespace: production
spec:
  reason: "zlib is present but the vulnerable path is never called"
  author: "platform-team"
  match:
    images:
      - "docker.io/library/nginx:*"
  vulnerabilities:
    - vulnerability:
        id: CVE-2023-45853
      status: not_affected
      justification: vulnerable_code_not_present
kubectl apply -f except-cve-2023-45853.yaml

The next scan moves the CVE out of the reported findings for matching workloads. Adding, changing, or removing an exception triggers a re-scan, so you do not need to wait for the next scheduled one.

Which statuses hide a finding

status is a VEX status, and it decides whether the CVE is suppressed or stays visible with extra context.

status finding is hidden notes
not_affected yes the vulnerability does not apply here
fixed yes already remediated in this image
affected only with actionStatement and a non-remediating response see below
under_investigation no recorded, still reported
anything else, including unset no unknown values never suppress

affected asserts that the vulnerability is real and applies, which is the opposite of a reason to hide it. It suppresses only when the entry also says no remediation is coming, which means both:

  • an actionStatement, which OpenVEX requires for every affected statement, and
  • a response list where every value is can_not_fix or will_not_fix

update, rollback and workaround_available all mean work is planned or in progress, so the finding stays visible until that is confirmed. Mixing them with will_not_fix also keeps it visible.

  vulnerabilities:
    - vulnerability:
        id: CVE-2024-1234
      status: affected
      actionStatement: "Accepted until the base image is replaced in Q3"
      response:
        - will_not_fix

Fields

Under spec.vulnerabilities[]:

  • vulnerability.id: the CVE ID. Required.
  • vulnerability.aliases: other IDs for the same vulnerability, matched as well as id.
  • status: one of the four values above. Required.
  • justification: why it does not apply, for not_affected. Use an OpenVEX justification such as vulnerable_code_not_present.
  • impactStatement: free text alternative to justification, for not_affected.
  • actionStatement: what is being done about it, for affected.
  • response: typed actions for affected: can_not_fix, will_not_fix, update, rollback, workaround_available.
  • expiredOnFix: when true, the entry stops applying to any finding that has a known fix.
  • expiresAt: RFC 3339 timestamp for this entry, overriding spec.expiresAt.
  • subcomponents: PURLs limiting the entry to specific packages.

spec.reason, spec.author and spec.expiresAt apply to the whole resource and are shared with posture entries.

Matching workloads and images

spec.match decides which workloads an entry applies to. Alongside resources, objectSelector and namespaceSelector (see Accepting risk with exceptions), vulnerability exceptions can match on images:

  • images: a list of glob patterns matched against the image reference.

Patterns are matched with path.Match, so * does not cross a /:

  match:
    images:
      - "docker.io/library/nginx:*"      # any nginx tag
      - "myregistry.io/team/*:v1.2.3"    # one tag, any repo under team
      - "docker.io/library/nginx"        # any tag or digest

A pattern with no registry is treated as Docker Hub, so nginx:1.25 matches docker.io/library/nginx:1.25 and nothing at another registry. To match a different registry, name it in the pattern.

Docker Hub can be written any of the usual ways. nginx:1.25, library/nginx:1.25, docker.io/nginx:1.25 and index.docker.io/library/nginx:1.25 all match the same image.

If match is empty, a SecurityException applies to its whole namespace and a ClusterSecurityException applies cluster-wide.

Limiting to a package

subcomponents scopes an entry to packages within the matched image, as PURLs. An unversioned PURL matches any version; a version-qualified one matches only that version.

    - vulnerability:
        id: CVE-2023-45853
      status: not_affected
      justification: vulnerable_code_not_present
      subcomponents:
        - "pkg:deb/debian/zlib1g@1:1.2.13.dfsg-1"

Without it, the entry applies to the whole image, so the same CVE is suppressed no matter which package it was found in.

Expiry

There are three ways an entry stops applying.

spec.expiresAt sets a deadline for the whole resource. expiresAt on an entry overrides it for that entry only:

spec:
  expiresAt: "2026-12-31T00:00:00Z"
  vulnerabilities:
    - vulnerability:
        id: CVE-2024-1111
      status: not_affected
      justification: vulnerable_code_not_present
      expiresAt: "2026-09-30T00:00:00Z"   # this one expires sooner

Expiry is evaluated at scan time. Once it has passed the entry is ignored and the finding is reported again; the resource itself is not deleted.

expiredOnFix expires an entry per finding rather than by date. When a fix becomes known for the CVE, the entry stops applying to that finding, so accepting a risk today does not silently keep hiding it once it becomes fixable.

Checking it worked

Each suppression is recorded as a VulnerabilitySuppressed Event on the resource that caused it, naming the CVE and the package:

kubectl describe securityexception except-cve-2023-45853 -n production

The scan result keeps suppressed CVEs rather than dropping them. They move from the reported findings into the ignored list on the VulnerabilityManifest, each carrying the rule that suppressed it, so an exception is auditable after the fact.

If nothing changes after a scan, check in this order:

  1. capabilities.riskAcceptance is enable
  2. the resource is in the same namespace as the workload, or is a ClusterSecurityException
  3. the image pattern matches (remember a registry-less pattern means Docker Hub)
  4. the status actually suppresses, per the table above
  5. the entry has not expired, by date or by expiredOnFix

VEX output

When VEX generation is enabled, these entries also shape the published VEX document: a suppressed CVE appears as a statement carrying its status, justification or impact statement, and action statement. See VEX document generation.