CVE-2026-41323
Description
Kyverno is a policy engine designed for cloud native platform engineering teams. Prior to versions 1.18.0-rc1, 1.17.2-rc1, and 1.16.4, Kyverno's apiCall feature in ClusterPolicy automatically attaches the admission controller's ServiceAccount token to outgoing HTTP requests. The service URL has no validation — it can point anywhere, including attacker-controlled servers. Since the admission controller SA has permissions to patch webhook configurations, a stolen token leads to full cluster compromise. Versions 1.18.0-rc1, 1.17.2-rc1, and 1.16.4 patch the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/kyverno/kyvernoGo | < 1.17.0 | 1.17.0 |
Affected products
2Patches
3c2eab00033e6fix: use scoped token for request authz (#15779) (#15801)
9 files changed · +148 −10
charts/kyverno/README.md+13 −0 modified@@ -252,6 +252,16 @@ The command removes all the Kubernetes components associated with the chart and The chart values are organised per component. +### Outbound API call token + +Kyverno projects a dedicated ServiceAccount token for outbound APICall and CEL HTTP requests. +Configure this token with `apiCallToken.*`: + +- `apiCallToken.audience` (default: `kyverno-svc.kyverno.io`) sets the OIDC `aud` claim expected by your receiving service. +- `apiCallToken.expirationSeconds` (default: `3600`) sets token lifetime before kubelet rotation. + +The default audience is Kyverno-specific so leaked tokens are not accepted by the Kubernetes API server. + ### Custom resource definitions | Key | Type | Default | Description | @@ -819,6 +829,9 @@ The chart values are organised per component. | rbac.roles.aggregate | object | `{"admin":true,"view":true}` | Aggregate ClusterRoles to Kubernetes default user-facing roles. For more information, see [User-facing roles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) | | openreports.enabled | bool | `false` | Enable OpenReports feature in controllers | | openreports.installCrds | bool | `false` | Whether to install CRDs from the upstream OpenReports chart. Setting this to true requires enabled to also be true. | +| apiCallToken | object | `{"audience":"kyverno-svc.kyverno.io","expirationSeconds":3600}` | Scoped token injected into outbound APICall and CEL HTTP requests. This token carries a custom audience so that if leaked to an external service it cannot be replayed against the Kubernetes API server. | +| apiCallToken.audience | string | `"kyverno-svc.kyverno.io"` | Audience for the projected token used in outbound requests. Set this to the audience your receiving service validates in the OIDC token's `aud` claim. The default is `kyverno-svc.kyverno.io`, which is a Kyverno-specific audience and prevents the token from being accepted by the Kubernetes API server. | +| apiCallToken.expirationSeconds | int | `3600` | Token lifetime in seconds for the projected outbound API call token. The default is `3600` (1 hour). The kubelet requests a replacement before the token expires, so lowering this reduces token lifetime while increasing rotation frequency. | | imagePullSecrets | object | `{}` | Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | | existingImagePullSecrets | list | `[]` | Existing Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | | customLabels | object | `{}` | Additional labels |
charts/kyverno/README.md.gotmpl+10 −0 modified@@ -252,6 +252,16 @@ The command removes all the Kubernetes components associated with the chart and The chart values are organised per component. +### Outbound API call token + +Kyverno projects a dedicated ServiceAccount token for outbound APICall and CEL HTTP requests. +Configure this token with `apiCallToken.*`: + +- `apiCallToken.audience` (default: `kyverno-svc.kyverno.io`) sets the OIDC `aud` claim expected by your receiving service. +- `apiCallToken.expirationSeconds` (default: `3600`) sets token lifetime before kubelet rotation. + +The default audience is Kyverno-specific so leaked tokens are not accepted by the Kubernetes API server. + {{- $other := list -}} {{- $crds := list -}} {{- $config := list -}}
charts/kyverno/templates/admission-controller/deployment.yaml+13 −0 modified@@ -292,6 +292,9 @@ spec: subPath: ca-certificates.crt {{- end }} {{- end }} + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount @@ -313,6 +316,16 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/templates/background-controller/deployment.yaml+13 −4 modified@@ -172,8 +172,10 @@ spec: securityContext: {{- toYaml . | nindent 12 }} {{- end }} - {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume (not $automountSAToken)}} volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume }} - name: ca-certificates mountPath: /etc/ssl/certs/ca-certificates.crt @@ -186,9 +188,17 @@ spec: mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true {{- end }} - {{- end }} - {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume (not $automountSAToken)}} volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data }} - name: ca-certificates configMap: @@ -202,7 +212,6 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} - {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/templates/cleanup-controller/deployment.yaml+16 −3 modified@@ -197,14 +197,27 @@ spec: readinessProbe: {{- tpl (toYaml .) $ | nindent 12 }} {{- end }} - {{- if not $automountSAToken }} volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true - {{- end }} - {{- if not $automountSAToken }} + {{- end }} volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} + {{- if not $automountSAToken }} - name: serviceaccount-token projected: defaultMode: 0444
charts/kyverno/templates/reports-controller/deployment.yaml+13 −0 modified@@ -197,6 +197,9 @@ spec: subPath: ca-certificates.crt {{- end }} {{- end }} + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount @@ -218,6 +221,16 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/values.yaml+15 −0 modified@@ -226,6 +226,21 @@ crds: # -- Toggle automounting of the ServiceAccount automountServiceAccountToken: true +# -- Scoped token injected into outbound APICall and CEL HTTP requests. +# This token carries a custom audience so that if leaked to an external service +# it cannot be replayed against the Kubernetes API server. +apiCallToken: + # -- Audience for the projected token used in outbound requests. + # Set this to the audience your receiving service validates in the OIDC token's + # `aud` claim. The default is `kyverno-svc.kyverno.io`, which is a Kyverno-specific + # audience and prevents the token from being accepted by the Kubernetes API server. + audience: "kyverno-svc.kyverno.io" + # -- Token lifetime in seconds for the projected outbound API call token. + # The default is `3600` (1 hour). The kubelet requests a replacement before the + # token expires, so lowering this reduces token lifetime while increasing rotation + # frequency. + expirationSeconds: 3600 + # Configuration config:
config/install-latest-testing.yaml+48 −0 modified@@ -70775,9 +70775,20 @@ spec: volumeMounts: - mountPath: /.sigstore name: sigstore + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true volumes: - name: sigstore emptyDir: {} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io" --- apiVersion: apps/v1 kind: Deployment @@ -70884,6 +70895,19 @@ spec: runAsNonRoot: true seccompProfile: type: RuntimeDefault + volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io" --- apiVersion: apps/v1 kind: Deployment @@ -71025,6 +71049,19 @@ spec: periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 + volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io" --- apiVersion: apps/v1 kind: Deployment @@ -71147,6 +71184,17 @@ spec: volumeMounts: - mountPath: /.sigstore name: sigstore + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true volumes: - name: sigstore emptyDir: {} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io"
pkg/engine/apicall/executor.go+7 −3 modified@@ -10,6 +10,7 @@ import ( "io" "net/http" "os" + "strings" "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" @@ -156,14 +157,17 @@ func (a *executor) addHTTPHeaders(req *http.Request, headers []kyvernov1.HTTPHea } func (a *executor) getToken() string { - fileName := "/var/run/secrets/kubernetes.io/serviceaccount/token" + // Use the scoped APICall token which carries a custom audience so that + // if leaked to an external service it cannot be replayed against the + // Kubernetes API server. + fileName := "/var/run/secrets/kyverno/apicall/token" b, err := os.ReadFile(fileName) if err != nil { - a.logger.Info("failed to read service account token", "path", fileName) + a.logger.V(2).Info("failed to read scoped APICall token, outbound request will proceed without Authorization header", "path", fileName, "error", err) return "" } - return string(b) + return strings.TrimSpace(string(b)) } func (a *executor) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, error) {
f70e8ac1e7acfix: use scoped token for request authz (#15779) (#15800)
9 files changed · +148 −10
charts/kyverno/README.md+13 −0 modified@@ -252,6 +252,16 @@ The command removes all the Kubernetes components associated with the chart and The chart values are organised per component. +### Outbound API call token + +Kyverno projects a dedicated ServiceAccount token for outbound APICall and CEL HTTP requests. +Configure this token with `apiCallToken.*`: + +- `apiCallToken.audience` (default: `kyverno-svc.kyverno.io`) sets the OIDC `aud` claim expected by your receiving service. +- `apiCallToken.expirationSeconds` (default: `3600`) sets token lifetime before kubelet rotation. + +The default audience is Kyverno-specific so leaked tokens are not accepted by the Kubernetes API server. + ### Custom resource definitions | Key | Type | Default | Description | @@ -854,6 +864,9 @@ The chart values are organised per component. | reportsServer.enabled | bool | `false` | Enable reports-server deployment alongside Kyverno | | reportsServer.waitForReady | bool | `true` | Wait for reports-server to be ready before starting Kyverno components | | reportsServer.readinessTimeout | string | `"300s"` | Timeout for waiting for reports-server readiness (as duration string, e.g. 300s, 5m) | +| apiCallToken | object | `{"audience":"kyverno-svc.kyverno.io","expirationSeconds":3600}` | Scoped token injected into outbound APICall and CEL HTTP requests. This token carries a custom audience so that if leaked to an external service it cannot be replayed against the Kubernetes API server. | +| apiCallToken.audience | string | `"kyverno-svc.kyverno.io"` | Audience for the projected token used in outbound requests. Set this to the audience your receiving service validates in the OIDC token's `aud` claim. The default is `kyverno-svc.kyverno.io`, which is a Kyverno-specific audience and prevents the token from being accepted by the Kubernetes API server. | +| apiCallToken.expirationSeconds | int | `3600` | Token lifetime in seconds for the projected outbound API call token. The default is `3600` (1 hour). The kubelet requests a replacement before the token expires, so lowering this reduces token lifetime while increasing rotation frequency. | | imagePullSecrets | object | `{}` | Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | | existingImagePullSecrets | list | `[]` | Existing Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | | customLabels | object | `{}` | Additional labels |
charts/kyverno/README.md.gotmpl+10 −0 modified@@ -252,6 +252,16 @@ The command removes all the Kubernetes components associated with the chart and The chart values are organised per component. +### Outbound API call token + +Kyverno projects a dedicated ServiceAccount token for outbound APICall and CEL HTTP requests. +Configure this token with `apiCallToken.*`: + +- `apiCallToken.audience` (default: `kyverno-svc.kyverno.io`) sets the OIDC `aud` claim expected by your receiving service. +- `apiCallToken.expirationSeconds` (default: `3600`) sets token lifetime before kubelet rotation. + +The default audience is Kyverno-specific so leaked tokens are not accepted by the Kubernetes API server. + {{- $other := list -}} {{- $crds := list -}} {{- $config := list -}}
charts/kyverno/templates/admission-controller/deployment.yaml+13 −0 modified@@ -296,6 +296,9 @@ spec: subPath: ca-certificates.crt {{- end }} {{- end }} + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount @@ -317,6 +320,16 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/templates/background-controller/deployment.yaml+13 −4 modified@@ -176,8 +176,10 @@ spec: securityContext: {{- toYaml . | nindent 12 }} {{- end }} - {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume (not $automountSAToken)}} volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume }} - name: ca-certificates mountPath: /etc/ssl/certs/ca-certificates.crt @@ -190,9 +192,17 @@ spec: mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true {{- end }} - {{- end }} - {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume (not $automountSAToken)}} volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data }} - name: ca-certificates configMap: @@ -206,7 +216,6 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} - {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/templates/cleanup-controller/deployment.yaml+16 −3 modified@@ -202,14 +202,27 @@ spec: readinessProbe: {{- tpl (toYaml .) $ | nindent 12 }} {{- end }} - {{- if not $automountSAToken }} volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount readOnly: true - {{- end }} - {{- if not $automountSAToken }} + {{- end }} volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} + {{- if not $automountSAToken }} - name: serviceaccount-token projected: defaultMode: 0444
charts/kyverno/templates/reports-controller/deployment.yaml+13 −0 modified@@ -201,6 +201,9 @@ spec: subPath: ca-certificates.crt {{- end }} {{- end }} + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount @@ -222,6 +225,16 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . | quote }} + {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/values.yaml+15 −0 modified@@ -241,6 +241,21 @@ crds: # -- Toggle automounting of the ServiceAccount automountServiceAccountToken: true +# -- Scoped token injected into outbound APICall and CEL HTTP requests. +# This token carries a custom audience so that if leaked to an external service +# it cannot be replayed against the Kubernetes API server. +apiCallToken: + # -- Audience for the projected token used in outbound requests. + # Set this to the audience your receiving service validates in the OIDC token's + # `aud` claim. The default is `kyverno-svc.kyverno.io`, which is a Kyverno-specific + # audience and prevents the token from being accepted by the Kubernetes API server. + audience: "kyverno-svc.kyverno.io" + # -- Token lifetime in seconds for the projected outbound API call token. + # The default is `3600` (1 hour). The kubelet requests a replacement before the + # token expires, so lowering this reduces token lifetime while increasing rotation + # frequency. + expirationSeconds: 3600 + # Configuration config:
config/install-latest-testing.yaml+48 −0 modified@@ -88659,9 +88659,20 @@ spec: volumeMounts: - mountPath: /.sigstore name: sigstore + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true volumes: - name: sigstore emptyDir: {} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io" --- apiVersion: apps/v1 kind: Deployment @@ -88770,6 +88781,19 @@ spec: runAsNonRoot: true seccompProfile: type: RuntimeDefault + volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io" --- apiVersion: apps/v1 kind: Deployment @@ -88914,6 +88938,19 @@ spec: periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 + volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io" --- apiVersion: apps/v1 kind: Deployment @@ -89038,6 +89075,17 @@ spec: volumeMounts: - mountPath: /.sigstore name: sigstore + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true volumes: - name: sigstore emptyDir: {} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: "kyverno-svc.kyverno.io"
pkg/engine/apicall/executor.go+7 −3 modified@@ -10,6 +10,7 @@ import ( "io" "net/http" "os" + "strings" "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" @@ -156,14 +157,17 @@ func (a *executor) addHTTPHeaders(req *http.Request, headers []kyvernov1.HTTPHea } func (a *executor) getToken() string { - fileName := "/var/run/secrets/kubernetes.io/serviceaccount/token" + // Use the scoped APICall token which carries a custom audience so that + // if leaked to an external service it cannot be replayed against the + // Kubernetes API server. + fileName := "/var/run/secrets/kyverno/apicall/token" b, err := os.ReadFile(fileName) if err != nil { - a.logger.Info("failed to read service account token", "path", fileName) + a.logger.V(2).Info("failed to read scoped APICall token, outbound request will proceed without Authorization header", "path", fileName, "error", err) return "" } - return string(b) + return strings.TrimSpace(string(b)) } func (a *executor) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, error) {
bc4f91c4801buse scoped token for request authz (#15779)
24 files changed · +417 −29
AGENTS.md+11 −0 modified@@ -94,6 +94,17 @@ Linting is configured via `.golangci.yml` (golangci-lint v2). Enabled linters in Formatters enabled: `gci`, `gofmt`, `gofumpt`, `goimports`. +**Pre-commit checklist (required for code changes):** + +- Run `make imports fmt` before committing. +- Run `make imports-check fmt-check` and ensure both pass. + +**Pre-PR checks (required before opening/updating a PR):** + +- Run `make codegen-all-code` then `make verify-codegen`. +- Run `./.tools/golangci-lint run` (install first with `make install-tools` if needed). + + ### Testing | Command | Description |
charts/kyverno/README.md+13 −0 modified@@ -252,6 +252,16 @@ The command removes all the Kubernetes components associated with the chart and The chart values are organised per component. +### Outbound API call token + +Kyverno projects a dedicated ServiceAccount token for outbound APICall and CEL HTTP requests. +Configure this token with `apiCallToken.*`: + +- `apiCallToken.audience` (default: `kyverno-svc.kyverno.io`) sets the OIDC `aud` claim expected by your receiving service. +- `apiCallToken.expirationSeconds` (default: `3600`) sets token lifetime before kubelet rotation. + +The default audience is Kyverno-specific so leaked tokens are not accepted by the Kubernetes API server. + ### Custom resource definitions | Key | Type | Default | Description | @@ -896,6 +906,9 @@ The chart values are organised per component. | reportsServer.enabled | bool | `false` | Enable reports-server deployment alongside Kyverno | | reportsServer.waitForReady | bool | `true` | Wait for reports-server to be ready before starting Kyverno components | | reportsServer.readinessTimeout | string | `"300s"` | Timeout for waiting for reports-server readiness (as duration string, e.g. 300s, 5m) | +| apiCallToken | object | `{"audience":"kyverno-svc.kyverno.io","expirationSeconds":3600}` | Scoped token injected into outbound APICall and CEL http requests. This token carries a custom audience so that if leaked to an external service it cannot be replayed against the Kubernetes API server. | +| apiCallToken.audience | string | `"kyverno-svc.kyverno.io"` | Audience for the projected token used in outbound requests. Set this to the audience your receiving service validates in the OIDC token's `aud` claim. The default is `kyverno-svc.kyverno.io`, which is a Kyverno-specific audience and prevents the token from being accepted by the Kubernetes API server. | +| apiCallToken.expirationSeconds | int | `3600` | Token lifetime in seconds for the projected outbound API call token. The default is `3600` (1 hour). The kubelet requests a replacement before the token expires, so lowering this reduces token lifetime while increasing rotation frequency. | | imagePullSecrets | object | `{}` | Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | | existingImagePullSecrets | list | `[]` | Existing Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | | customLabels | object | `{}` | Additional labels |
charts/kyverno/README.md.gotmpl+10 −0 modified@@ -252,6 +252,16 @@ The command removes all the Kubernetes components associated with the chart and The chart values are organised per component. +### Outbound API call token + +Kyverno projects a dedicated ServiceAccount token for outbound APICall and CEL HTTP requests. +Configure this token with `apiCallToken.*`: + +- `apiCallToken.audience` (default: `kyverno-svc.kyverno.io`) sets the OIDC `aud` claim expected by your receiving service. +- `apiCallToken.expirationSeconds` (default: `3600`) sets token lifetime before kubelet rotation. + +The default audience is Kyverno-specific so leaked tokens are not accepted by the Kubernetes API server. + {{- $other := list -}} {{- $crds := list -}} {{- $config := list -}}
charts/kyverno/templates/admission-controller/deployment.yaml+13 −0 modified@@ -293,6 +293,9 @@ spec: subPath: ca-certificates.crt {{- end }} {{- end }} + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount @@ -317,6 +320,16 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . }} + {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/templates/background-controller/deployment.yaml+13 −4 modified@@ -185,8 +185,10 @@ spec: securityContext: {{- toYaml . | nindent 12 }} {{- end }} - {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume (not $automountSAToken) .Values.backgroundController.extraVolumeMounts }} volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume }} - name: ca-certificates mountPath: /etc/ssl/certs/ca-certificates.crt @@ -202,9 +204,17 @@ spec: {{- with .Values.backgroundController.extraVolumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} - {{- end }} - {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data .Values.backgroundController.caCertificates.volume .Values.global.caCertificates.volume (not $automountSAToken) .Values.backgroundController.extraVolumes }} volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . }} + {{- end }} {{- if or .Values.backgroundController.caCertificates.data .Values.global.caCertificates.data }} - name: ca-certificates configMap: @@ -218,7 +228,6 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} - {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/templates/cleanup-controller/deployment.yaml+13 −4 modified@@ -200,8 +200,10 @@ spec: readinessProbe: {{- tpl (toYaml .) $ | nindent 12 }} {{- end }} - {{- if or (not $automountSAToken) .Values.cleanupController.extraVolumeMounts }} volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount @@ -210,9 +212,17 @@ spec: {{- with .Values.cleanupController.extraVolumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} - {{- end }} - {{- if or (not $automountSAToken) .Values.cleanupController.extraVolumes }} volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . }} + {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected: @@ -239,6 +249,5 @@ spec: {{- with .Values.cleanupController.extraVolumes }} {{- toYaml . | nindent 8 }} {{- end }} - {{- end }} {{- end -}} {{- end -}}
charts/kyverno/templates/reports-controller/deployment.yaml+13 −0 modified@@ -209,6 +209,9 @@ spec: subPath: ca-certificates.crt {{- end }} {{- end }} + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true {{- if not $automountSAToken }} - name: serviceaccount-token mountPath: /var/run/secrets/kubernetes.io/serviceaccount @@ -233,6 +236,16 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- end }} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: {{ .Values.apiCallToken.expirationSeconds | default 3600 }} + {{- with .Values.apiCallToken.audience }} + audience: {{ . }} + {{- end }} {{- if not $automountSAToken }} - name: serviceaccount-token projected:
charts/kyverno/values.yaml+15 −0 modified@@ -252,6 +252,21 @@ crds: # If not set, the token will have no audience restriction. audience: "" +# -- Scoped token injected into outbound APICall and CEL http requests. +# This token carries a custom audience so that if leaked to an external service +# it cannot be replayed against the Kubernetes API server. +apiCallToken: + # -- Audience for the projected token used in outbound requests. + # Set this to the audience your receiving service validates in the OIDC token's + # `aud` claim. The default is `kyverno-svc.kyverno.io`, which is a Kyverno-specific + # audience and prevents the token from being accepted by the Kubernetes API server. + audience: "kyverno-svc.kyverno.io" + # -- Token lifetime in seconds for the projected outbound API call token. + # The default is `3600` (1 hour). The kubelet requests a replacement before the + # token expires, so lowering this reduces token lifetime while increasing rotation + # frequency. + expirationSeconds: 3600 + # Configuration config:
cmd/background-controller/main.go+1 −0 modified@@ -163,6 +163,7 @@ func main() { ) // parse flags internal.ParseFlags(appConfig) + apicall.SetScopedTokenClientTimeout(apiCallTimeout) var wg wait.Group func() { // setup
cmd/cleanup-controller/main.go+2 −0 modified@@ -28,6 +28,7 @@ import ( genericwebhookcontroller "github.com/kyverno/kyverno/pkg/controllers/generic/webhook" globalcontextcontroller "github.com/kyverno/kyverno/pkg/controllers/globalcontext" ttlcontroller "github.com/kyverno/kyverno/pkg/controllers/ttl" + "github.com/kyverno/kyverno/pkg/engine/apicall" "github.com/kyverno/kyverno/pkg/event" "github.com/kyverno/kyverno/pkg/globalcontext/store" "github.com/kyverno/kyverno/pkg/informers" @@ -125,6 +126,7 @@ func main() { ) // parse flags internal.ParseFlags(appConfig) + apicall.SetScopedTokenClientTimeout(apiCallTimeout) var wg wait.Group func() { // setup
cmd/kyverno/main.go+1 −0 modified@@ -408,6 +408,7 @@ func main() { ) // parse flags internal.ParseFlags(appConfig) + apicall.SetScopedTokenClientTimeout(apiCallTimeout) var wg wait.Group func() { // setup
cmd/reports-controller/main.go+1 −0 modified@@ -330,6 +330,7 @@ func main() { internal.WithDefaultQps(300), internal.WithDefaultBurst(300), ) + apicall.SetScopedTokenClientTimeout(apiCallTimeout) var wg wait.Group func() { // setup
config/install-latest-testing.yaml+48 −0 modified@@ -88753,9 +88753,20 @@ spec: volumeMounts: - mountPath: /.sigstore name: sigstore + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true volumes: - name: sigstore emptyDir: {} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: kyverno-svc.kyverno.io --- apiVersion: apps/v1 kind: Deployment @@ -88870,6 +88881,19 @@ spec: runAsUser: 65534 seccompProfile: type: RuntimeDefault + volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: kyverno-svc.kyverno.io --- apiVersion: apps/v1 kind: Deployment @@ -89019,6 +89043,19 @@ spec: periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 + volumeMounts: + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true + volumes: + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: kyverno-svc.kyverno.io --- apiVersion: apps/v1 kind: Deployment @@ -89148,6 +89185,17 @@ spec: volumeMounts: - mountPath: /.sigstore name: sigstore + - name: apicall-token + mountPath: /var/run/secrets/kyverno/apicall + readOnly: true volumes: - name: sigstore emptyDir: {} + - name: apicall-token + projected: + defaultMode: 0444 + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + audience: kyverno-svc.kyverno.io
pkg/background/mpol/env.go+2 −1 modified@@ -5,6 +5,7 @@ import ( "github.com/google/cel-go/common/types" "github.com/kyverno/kyverno/pkg/cel/compiler" "github.com/kyverno/kyverno/pkg/cel/libs" + "github.com/kyverno/kyverno/pkg/engine/apicall" "github.com/kyverno/sdk/cel/libs/globalcontext" "github.com/kyverno/sdk/cel/libs/hash" "github.com/kyverno/sdk/cel/libs/http" @@ -65,7 +66,7 @@ func BuildMpolTargetEvalEnv(libsctx libs.Context, namespace string) (*cel.Env, e globalcontext.Latest(), ), http.Lib( - http.Context{ContextInterface: http.NewHTTP(nil)}, + http.Context{ContextInterface: http.NewHTTP(apicall.NewScopedTokenClient())}, http.Latest(), ), image.Lib(
pkg/cel/policies/dpol/compiler/compiler.go+2 −1 modified@@ -8,6 +8,7 @@ import ( policiesv1beta1 "github.com/kyverno/api/api/policies.kyverno.io/v1beta1" "github.com/kyverno/kyverno/pkg/cel/compiler" "github.com/kyverno/kyverno/pkg/cel/libs" + "github.com/kyverno/kyverno/pkg/engine/apicall" "github.com/kyverno/sdk/cel/libs/globalcontext" "github.com/kyverno/sdk/cel/libs/gzip" "github.com/kyverno/sdk/cel/libs/hash" @@ -144,7 +145,7 @@ func (c *compilerImpl) createBaseDpolEnv(libsctx libs.Context, namespace string) globalcontext.Latest(), ), http.Lib( - http.Context{ContextInterface: http.NewHTTP(nil)}, + http.Context{ContextInterface: http.NewHTTP(apicall.NewScopedTokenClient())}, http.Latest(), ), image.Lib(
pkg/cel/policies/gpol/compiler/compiler.go+2 −1 modified@@ -10,6 +10,7 @@ import ( policiesv1beta1 "github.com/kyverno/api/api/policies.kyverno.io/v1beta1" "github.com/kyverno/kyverno/pkg/cel/compiler" "github.com/kyverno/kyverno/pkg/cel/libs" + "github.com/kyverno/kyverno/pkg/engine/apicall" "github.com/kyverno/sdk/cel/libs/generator" "github.com/kyverno/sdk/cel/libs/globalcontext" "github.com/kyverno/sdk/cel/libs/gzip" @@ -95,7 +96,7 @@ func createBaseGpolEnv(libsctx libs.Context, namespace string) (*environment.Env globalcontext.Latest(), ), http.Lib( - http.Context{ContextInterface: http.NewHTTP(nil)}, + http.Context{ContextInterface: http.NewHTTP(apicall.NewScopedTokenClient())}, http.Latest(), ), resource.Lib(
pkg/cel/policies/mpol/compiler/compiler.go+2 −1 modified@@ -10,6 +10,7 @@ import ( policiesv1beta1 "github.com/kyverno/api/api/policies.kyverno.io/v1beta1" compiler "github.com/kyverno/kyverno/pkg/cel/compiler" "github.com/kyverno/kyverno/pkg/cel/libs" + "github.com/kyverno/kyverno/pkg/engine/apicall" "github.com/kyverno/sdk/cel/libs/generator" "github.com/kyverno/sdk/cel/libs/globalcontext" "github.com/kyverno/sdk/cel/libs/gzip" @@ -175,7 +176,7 @@ func newExtendedEnv(libCtx libs.Context, namespace string) (*cel.Env, *compiler. globalcontext.Latest(), ), http.Lib( - http.Context{ContextInterface: http.NewHTTP(nil)}, + http.Context{ContextInterface: http.NewHTTP(apicall.NewScopedTokenClient())}, http.Latest(), ), resource.Lib(
pkg/cel/policies/vpol/compiler/compiler.go+2 −1 modified@@ -12,6 +12,7 @@ import ( policiesv1beta1 "github.com/kyverno/api/api/policies.kyverno.io/v1beta1" "github.com/kyverno/kyverno/pkg/cel/compiler" "github.com/kyverno/kyverno/pkg/cel/libs" + "github.com/kyverno/kyverno/pkg/engine/apicall" "github.com/kyverno/kyverno/pkg/toggle" "github.com/kyverno/sdk/cel/libs/globalcontext" "github.com/kyverno/sdk/cel/libs/gzip" @@ -253,7 +254,7 @@ func (c *compilerImpl) createBaseVpolEnv(libsctx libs.Context, namespace string) globalcontext.Latest(), ), http.Lib( - http.Context{ContextInterface: http.NewHTTP(nil)}, + http.Context{ContextInterface: http.NewHTTP(apicall.NewScopedTokenClient())}, http.Latest(), ), resource.Lib(
pkg/engine/apicall/apiCall_test.go+8 −1 modified@@ -152,6 +152,9 @@ func Test_servicePostRequest(t *testing.T) { Method: "POST", Service: &kyvernov1.ServiceCall{ URL: s.URL + "/resource", + Headers: []kyvernov1.HTTPHeader{ + {Key: "Authorization", Value: "Bearer 1234567890"}, + }, }, }, }, @@ -285,6 +288,7 @@ func Test_serviceHeaders(t *testing.T) { Service: &kyvernov1.ServiceCall{ URL: s.URL + "/resource", Headers: []kyvernov1.HTTPHeader{ + {Key: "Authorization", Value: "Bearer 1234567890"}, {Key: "Content-Type", Value: "application/json"}, {Key: "Custom-Key", Value: "CustomVal"}, }, @@ -302,7 +306,7 @@ func Test_serviceHeaders(t *testing.T) { var responseHeaders map[string][]string err = json.Unmarshal(data, &responseHeaders) assert.NilError(t, err) - assert.Equal(t, 4, len(responseHeaders)) + assert.Equal(t, 5, len(responseHeaders)) assert.Equal(t, "application/json", responseHeaders["Content-Type"][0]) assert.Equal(t, "CustomVal", responseHeaders["Custom-Key"][0]) } @@ -406,6 +410,9 @@ func Test_contextCancellation(t *testing.T) { Method: "GET", Service: &kyvernov1.ServiceCall{ URL: s.URL, + Headers: []kyvernov1.HTTPHeader{ + {Key: "Authorization", Value: "Bearer 1234567890"}, + }, }, }, },
pkg/engine/apicall/executor.go+1 −14 modified@@ -9,7 +9,6 @@ import ( "fmt" "io" "net/http" - "os" "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" @@ -153,26 +152,14 @@ func (a *executor) addHTTPHeaders(req *http.Request, headers []kyvernov1.HTTPHea } if req.Header.Get("Authorization") == "" { - token := a.getToken() - if token != "" { + if token, ok := readScopedToken(); ok && token != "" { req.Header.Add("Authorization", "Bearer "+token) } } return nil } -func (a *executor) getToken() string { - fileName := "/var/run/secrets/kubernetes.io/serviceaccount/token" - b, err := os.ReadFile(fileName) - if err != nil { - a.logger.Info("failed to read service account token", "path", fileName) - return "" - } - - return string(b) -} - func (a *executor) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, error) { timeout := a.config.GetTimeout() if service == nil || service.CABundle == "" {
pkg/engine/apicall/executor_test.go+31 −0 modified@@ -4,6 +4,8 @@ import ( "context" "errors" "io" + "net/http" + "net/http/httptest" "testing" "github.com/go-logr/logr" @@ -120,6 +122,35 @@ func Test_ExecuteK8sAPICall_Success(t *testing.T) { assert.Equal(t, string(data), "{}") } +func Test_ExecuteServiceCall_AllowsMissingScopedTokenWhenAuthorizationMissing(t *testing.T) { + missingTokenPath := scopedTokenPath + ".missing" + oldPath := scopedTokenPath + scopedTokenPath = missingTokenPath + t.Cleanup(func() { + scopedTokenPath = oldPath + }) + + var gotAuth string + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok":true}`)) + })) + defer s.Close() + + executor := NewExecutor(logr.Discard(), "test-call", &mockClient{}, apiConfig) + call := &kyvernov1.APICall{ + Method: "GET", + Service: &kyvernov1.ServiceCall{ + URL: s.URL, + }, + } + + _, err := executor.Execute(context.TODO(), call) + assert.NilError(t, err) + assert.Equal(t, gotAuth, "") +} + // Helper function to check if string contains substring func contains(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ {
pkg/engine/apicall/httpclient.go+86 −0 added@@ -0,0 +1,86 @@ +package apicall + +import ( + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/kyverno/kyverno/pkg/tracing" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "k8s.io/klog/v2" +) + +// scopedTokenPath is the mount path of the projected ServiceAccount token used +// for outbound APICall and CEL http requests. Unlike the default SA token, this +// token carries a custom audience (configured via .Values.apiCallToken.audience) +// so that if it is leaked to an external service it cannot be replayed against +// the Kubernetes API server. +const ( + scopedTokenPathEnvVar = "KYVERNO_SCOPED_TOKEN_PATH" // #nosec G101 false positive: environment variable name + defaultScopedTokenPath = "/var/run/secrets/kyverno/apicall/token" // #nosec G101 false positive: token file path, not a credential + defaultScopedTokenClientTimeout = 30 * time.Second +) + +var ( + scopedTokenPath = getScopedTokenPath() + scopedTokenClientTimeout = defaultScopedTokenClientTimeout + scopedTokenReadWarningOnce sync.Once +) + +func getScopedTokenPath() string { + if path := os.Getenv(scopedTokenPathEnvVar); path != "" { + return path + } + return defaultScopedTokenPath +} + +// SetScopedTokenClientTimeout configures timeout for outbound CEL HTTP calls +// performed through the scoped token client. A value of 0 disables timeout. +func SetScopedTokenClientTimeout(timeout time.Duration) { + scopedTokenClientTimeout = timeout +} + +func readScopedToken() (string, bool) { + b, err := os.ReadFile(scopedTokenPath) + if err != nil { + scopedTokenReadWarningOnce.Do(func() { + if os.IsNotExist(err) { + klog.Warningf("optional scoped APICall token not found at %s; outbound calls will proceed without Authorization header unless explicitly provided", scopedTokenPath) + } else { + klog.Warningf("failed to read optional scoped APICall token at %s: %v", scopedTokenPath, err) + } + }) + return "", false + } + return strings.TrimSpace(string(b)), true +} + +// scopedTokenClient wraps http.Client and injects the scoped APICall token as +// an Authorization Bearer header whenever the caller has not already set one. +type scopedTokenClient struct { + inner *http.Client +} + +// NewScopedTokenClient returns a *scopedTokenClient that injects the scoped +// APICall token into outbound HTTP requests. This concrete type satisfies the +// ClientInterface expected by github.com/kyverno/sdk/cel/libs/http.NewHTTP. +func NewScopedTokenClient() *scopedTokenClient { + return &scopedTokenClient{ + inner: &http.Client{ + Transport: tracing.Transport(http.DefaultTransport, otelhttp.WithFilter(tracing.RequestFilterIsInSpan)), + Timeout: scopedTokenClientTimeout, + }, + } +} + +func (c *scopedTokenClient) Do(req *http.Request) (*http.Response, error) { + if req.Header.Get("Authorization") == "" { + token, ok := readScopedToken() + if ok && token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + } + return c.inner.Do(req) +}
pkg/engine/apicall/httpclient_test.go+125 −0 added@@ -0,0 +1,125 @@ +package apicall + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "gotest.tools/assert" +) + +func Test_NewScopedTokenClient_Defaults(t *testing.T) { + withScopedTokenClientTimeout(t, defaultScopedTokenClientTimeout) + client := NewScopedTokenClient() + + assert.Equal(t, client.inner.Timeout, defaultScopedTokenClientTimeout) + _, ok := client.inner.Transport.(*otelhttp.Transport) + assert.Check(t, ok) +} + +func Test_NewScopedTokenClient_UsesConfiguredTimeout(t *testing.T) { + withScopedTokenClientTimeout(t, 7*time.Second) + client := NewScopedTokenClient() + + assert.Equal(t, client.inner.Timeout, 7*time.Second) +} + +func withScopedTokenClientTimeout(t *testing.T, timeout time.Duration) { + t.Helper() + old := scopedTokenClientTimeout + SetScopedTokenClientTimeout(timeout) + t.Cleanup(func() { + SetScopedTokenClientTimeout(old) + }) +} + +func withScopedTokenPath(t *testing.T, path string) { + t.Helper() + old := scopedTokenPath + scopedTokenPath = path + t.Cleanup(func() { + scopedTokenPath = old + }) +} + +func Test_scopedTokenClient_Do_SetsAuthorizationWhenAbsent(t *testing.T) { + tokenPath := filepath.Join(t.TempDir(), "token") + assert.NilError(t, os.WriteFile(tokenPath, []byte(" test-token\n"), 0o600)) + withScopedTokenPath(t, tokenPath) + + var gotAuth string + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer s.Close() + + client := NewScopedTokenClient() + client.inner = s.Client() + + req, err := http.NewRequest(http.MethodGet, s.URL, nil) + assert.NilError(t, err) + + resp, err := client.Do(req) + assert.NilError(t, err) + defer resp.Body.Close() + _, _ = io.ReadAll(resp.Body) + + assert.Equal(t, gotAuth, "Bearer test-token") +} + +func Test_scopedTokenClient_Do_DoesNotOverrideAuthorizationWhenPresent(t *testing.T) { + missingTokenPath := filepath.Join(t.TempDir(), "missing-token") + withScopedTokenPath(t, missingTokenPath) + + var gotAuth string + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer s.Close() + + client := NewScopedTokenClient() + client.inner = s.Client() + + req, err := http.NewRequest(http.MethodGet, s.URL, nil) + assert.NilError(t, err) + req.Header.Set("Authorization", "Bearer provided-token") + + resp, err := client.Do(req) + assert.NilError(t, err) + defer resp.Body.Close() + _, _ = io.ReadAll(resp.Body) + + assert.Equal(t, gotAuth, "Bearer provided-token") +} + +func Test_scopedTokenClient_Do_TokenMissingDoesNotFailRequest(t *testing.T) { + missingTokenPath := filepath.Join(t.TempDir(), "missing-token") + withScopedTokenPath(t, missingTokenPath) + + var gotAuth string + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer s.Close() + + client := NewScopedTokenClient() + client.inner = s.Client() + + req, err := http.NewRequest(http.MethodGet, s.URL, nil) + assert.NilError(t, err) + + resp, err := client.Do(req) + assert.NilError(t, err) + defer resp.Body.Close() + _, _ = io.ReadAll(resp.Body) + + assert.Equal(t, gotAuth, "") +}
pkg/image/verification/evaluator/compiler.go+2 −1 modified@@ -11,6 +11,7 @@ import ( engine "github.com/kyverno/kyverno/pkg/cel/compiler" "github.com/kyverno/kyverno/pkg/cel/libs" "github.com/kyverno/kyverno/pkg/cel/libs/imageverify" + "github.com/kyverno/kyverno/pkg/engine/apicall" ivpolvar "github.com/kyverno/kyverno/pkg/image/verification/variables" "github.com/kyverno/kyverno/pkg/toggle" "github.com/kyverno/sdk/cel/libs/globalcontext" @@ -212,7 +213,7 @@ func (c *compilerImpl) createBaseIvpolEnv(libsctx libs.Context, ivpol policiesv1 globalcontext.Latest(), ), http.Lib( - http.Context{ContextInterface: http.NewHTTP(nil)}, + http.Context{ContextInterface: http.NewHTTP(apicall.NewScopedTokenClient())}, http.Latest(), ), image.Lib(
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/kyverno/kyverno/commit/bc4f91c4801b1eaa2edc0a14e2f1b0af8cf0c1f5nvdPatchWEB
- github.com/kyverno/kyverno/commit/c2eab00033e635bda4e4efb58c1b472b41728bb6nvdPatchWEB
- github.com/kyverno/kyverno/commit/f70e8ac1e7acd2e3844f9553e4a884f07f953de0nvdPatchWEB
- github.com/advisories/GHSA-f9g8-6ppc-pqq4ghsaADVISORY
- github.com/kyverno/kyverno/security/advisories/GHSA-f9g8-6ppc-pqq4nvdVendor AdvisoryExploitWEB
- nvd.nist.gov/vuln/detail/CVE-2026-41323ghsaADVISORY
News mentions
0No linked articles in our index yet.