VYPR
High severity8.1NVD Advisory· Published Apr 24, 2026· Updated Apr 27, 2026

CVE-2026-41323

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.

PackageAffected versionsPatched versions
github.com/kyverno/kyvernoGo
< 1.17.01.17.0

Affected products

2
  • Kyverno/Kyverno2 versions
    cpe:2.3:a:kyverno:kyverno:*:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:a:kyverno:kyverno:*:*:*:*:*:*:*:*range: >=1.17.0,<1.17.2
    • cpe:2.3:a:kyverno:kyverno:*:-:*:*:*:*:*:*range: <1.16.4

Patches

3
c2eab00033e6

fix: use scoped token for request authz (#15779) (#15801)

https://github.com/kyverno/kyvernoJim BugwadiaApr 12, 2026via ghsa
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) {
    
f70e8ac1e7ac

fix: use scoped token for request authz (#15779) (#15800)

https://github.com/kyverno/kyvernoJim BugwadiaApr 9, 2026via ghsa
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) {
    
bc4f91c4801b

use scoped token for request authz (#15779)

https://github.com/kyverno/kyvernoJim BugwadiaApr 3, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.