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
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
responselist where every value iscan_not_fixorwill_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 asid.status: one of the four values above. Required.justification: why it does not apply, fornot_affected. Use an OpenVEX justification such asvulnerable_code_not_present.impactStatement: free text alternative tojustification, fornot_affected.actionStatement: what is being done about it, foraffected.response: typed actions foraffected: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, overridingspec.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:
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:
capabilities.riskAcceptanceisenable- the resource is in the same namespace as the workload, or is a
ClusterSecurityException - the image pattern matches (remember a registry-less pattern means Docker Hub)
- the status actually suppresses, per the table above
- 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.