Skip to content

feat(ivpol): add CEL expression support for keyless identity fields#15479

Open
jzeng4 wants to merge 5 commits intokyverno:mainfrom
jzeng4:juzeng/cel
Open

feat(ivpol): add CEL expression support for keyless identity fields#15479
jzeng4 wants to merge 5 commits intokyverno:mainfrom
jzeng4:juzeng/cel

Conversation

@jzeng4
Copy link
Copy Markdown

@jzeng4 jzeng4 commented Mar 7, 2026

Explanation

This PR adds CEL expression support to the issuer, subject, issuerRegExp, and subjectRegExp fields of the Identity type in ImageValidatingPolicy's keyless (Fulcio/Sigstore) configuration. Previously these fields only accepted static strings, which forced users to write one policy per image when they needed per-image signing identity pinning. With this change, each field accepts either a plain string (unchanged) or a {expression: "..."} CEL expression that is evaluated at admission time with full access to the request context — enabling a single policy to enforce precise per-image signing identities across an entire organization.

This is a backward-compatible addition: all existing policies using plain string values continue to work without modification.

Related issue

Fixes #15398
Depend on kyverno/api#64

Milestone of this PR

Documentation (required for features)

My PR contains new or altered behavior to Kyverno.

What type of PR is this

/kind api-change
/kind feature

Proposed Changes

The Identity struct fields (issuer, subject, issuerRegExp, subjectRegExp) are changed from plain string to *StringOrExpression — the same union type already used for cert, certChain, and Notary certs fields. A custom UnmarshalJSON on StringOrExpression ensures that a plain YAML string (e.g. issuer: "https://...") is transparently decoded into StringOrExpression{Value: "..."}, so no existing policies need to be updated.

At policy compile time, any {expression: "..."} values are compiled into CEL programs. At admission time, those programs are evaluated against the full request context (including object, images, request, and variables) and their string results are substituted as the identity values passed to cosign's CheckOpts.Identities. This means subject and subjectRegExp are matched against the Fulcio certificate's SAN URI, and issuer/issuerRegExp against the OIDC issuer OID extension, exactly as before — the only difference is the values can now be computed rather than hardcoded.

Changes:

  • api/policies.kyverno.io/v1alpha1/imagevalidating_policy.go — change Identity
    fields to *StringOrExpression; add backward-compatible UnmarshalJSON on
    StringOrExpression
  • api/policies.kyverno.io/v1alpha1/zz_generated.deepcopy.go — update
    Identity.DeepCopyInto and Keyless.DeepCopyInto for the new pointer fields
  • pkg/imageverification/variables/attestors.go — compile and evaluate CEL expressions
    for identity fields
  • pkg/imageverification/imageverifiers/cosign/opts.go — read .Value from pointer
    fields when building cosign.CheckOpts

Proof Manifests

Example 1: Backward-compatible plain string (no change needed)

Existing policies continue to work unchanged:

apiVersion: policies.kyverno.io/v1alpha1
kind: ImageValidatingPolicy
metadata:
  name: verify-image-keyless-static
spec:
  failurePolicy: Fail
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  attestors:
    - name: sigstore
      cosign:
        keyless:
          identities:
            - issuer: "https://token.actions.githubusercontent.com"
              subject: "https://github.com/my-org/my-repo/.github/workflows/release.yml@refs/heads/main"
  validations:
    - expression: >-
        images.containers.map(image, verifyImageSignatures(image, [attestors.sigstore])).all(e, e > 0)
      message: Image signature verification failed.

Example 2: Derive subject from a pod label (per-repo identity, one policy for all services)

The expected signer workflow is derived from the github-repo pod label at admission
time. subject and subjectRegExp match against the Fulcio certificate's SAN URI;
issuer/issuerRegExp match against the OIDC issuer OID extension on the certificate.

apiVersion: policies.kyverno.io/v1alpha1
kind: ImageValidatingPolicy
metadata:
  name: verify-image-keyless-cel-identity
spec:
  failurePolicy: Fail
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  attestors:
    - name: sigstore
      cosign:
        keyless:
          identities:
            - issuer:
                value: "https://token.actions.githubusercontent.com"
              subject:
                expression: >-
                  "https://github.com/" + object.metadata.labels["github-repo"] +
                  "/.github/workflows/release.yml@refs/heads/main"
  validations:
    - expression: >-
        images.containers.map(image, verifyImageSignatures(image, [attestors.sigstore])).all(e, e > 0)
      message: >-
        Image signature verification failed. The image must be signed by the
        GitHub Actions workflow for the repository specified in the 'github-repo' label.

Pod that is admitted (label matches the signing identity):

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    github-repo: "my-org/my-app"
spec:
  containers:
    - name: app
      image: ghcr.io/my-org/my-app:v1.2.3

Pod that is denied (label does not match the actual signing identity):

apiVersion: v1
kind: Pod
metadata:
  name: untrusted-app
  labels:
    github-repo: "my-org/other-repo"
spec:
  containers:
    - name: app
      image: ghcr.io/my-org/my-app:v1.2.3

Example 3: Derive subject from the namespace

Enforce a per-team signing convention where each namespace maps to a GitHub org team:

apiVersion: policies.kyverno.io/v1alpha1
kind: ImageValidatingPolicy
metadata:
  name: verify-image-keyless-by-namespace
spec:
  failurePolicy: Fail
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  attestors:
    - name: sigstore
      cosign:
        keyless:
          identities:
            - issuer:
                value: "https://token.actions.githubusercontent.com"
              subjectRegExp:
                expression: >-
                  "^https://github\\.com/my-org/" + object.metadata.namespace + "/.*"
  validations:
    - expression: >-
        images.containers.map(image, verifyImageSignatures(image, [attestors.sigstore])).all(e, e > 0)
      message: >-
        Image must be signed by a workflow belonging to the GitHub repository
        matching this pod's namespace.

Example 4: Derive subject from the image reference (single-container pods)

Use images.containers[0] to extract the org and repo from the image reference itself.
Note: this approach works reliably for single-container pods; for multi-container pods,
use a pod label (Example 2) or policy variable instead.

apiVersion: policies.kyverno.io/v1alpha1
kind: ImageValidatingPolicy
metadata:
  name: verify-image-keyless-from-image-ref
spec:
  failurePolicy: Fail
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  attestors:
    - name: sigstore
      cosign:
        keyless:
          identities:
            - issuer:
                value: "https://token.actions.githubusercontent.com"
              subjectRegExp:
                expression: >-
                  "^https://github\\.com/" +
                  images.containers[0].split("/")[1] + "/" +
                  images.containers[0].split("/")[2].split(":")[0] + "/.*"
  validations:
    - expression: >-
        images.containers.map(image, verifyImageSignatures(image, [attestors.sigstore])).all(e, e > 0)
      message: >-
        Image must be signed by a workflow from the same GitHub repository as the image.

Available CEL variables in identity expressions

Identity expressions are evaluated once per admission request with the same context
available to validations expressions:

Variable Type Description
object dynamic The K8s resource being admitted
images map<string, list<string>> e.g. images.containers, images.initContainers
request object Admission request (.namespace, .name, .userInfo)
namespaceObject object The namespace object
variables object Policy-defined variables

Checklist

  • I have read the contributing guidelines.
  • I have read the AI Usage Policy. If I used AI assistance, I have disclosed it in my commit(s) (e.g., via Co-authored-by or Assisted-by trailer).
  • I have read the PR documentation guide and followed the process including adding proof manifests to this PR.
  • This is a bug fix and I have added unit tests that prove my fix is effective.
  • This is a feature and I have added CLI tests that are applicable.
  • My PR needs to be cherry picked to a specific release branch which is .
  • My PR contains new or altered behavior to Kyverno and
    • CLI support should be added and my PR doesn't contain that functionality.

Further Comments

The design deliberately mirrors the existing StringOrExpression pattern used for
cert, certChain, and Notary certs fields, keeping the API consistent. The
UnmarshalJSON approach for backward compatibility is the same technique used elsewhere
in the codebase.

Identity expressions are resolved once per admission request (not per image). For
per-image identity derived from the image reference itself, images.containers[0] can
be used for single-container pods; for multi-container workloads, the recommended
approach is to encode the expected identity in a pod label or annotation and reference it

@jzeng4 jzeng4 requested a review from a team as a code owner March 7, 2026 22:20
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 7, 2026
@github-actions github-actions Bot added kind/tests Unit / integration tests kind/dependencies go.mod / go.sum changes area/image-verify Image verification labels Mar 7, 2026
@jzeng4 jzeng4 changed the title initial feat(ivpol): add CEL expression support for keyless identity fields Mar 7, 2026
jzeng4 added 3 commits March 7, 2026 15:05
Signed-off-by: Junyuan Zeng <jzeng04@gmail.com>
Signed-off-by: Junyuan Zeng <jzeng04@gmail.com>
Signed-off-by: Junyuan Zeng <jzeng04@gmail.com>
@fjogeleit
Copy link
Copy Markdown
Member

fjogeleit commented Mar 10, 2026

this PR, if I understand it correctly, implements a breaking change into the IVPOL API, which was marked as stable in 1.17.0. So we can't merge this without introducing a it as a new API Version or in a backward compatible way.

In other situations we have for example dedicated fields for static and expressions like:

message and messageExpr

You also have to make a related PR in the API repo instead of replacing the lib path.

Copy link
Copy Markdown
Member

@fjogeleit fjogeleit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IVPOL API breaking change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/image-verify Image verification kind/dependencies go.mod / go.sum changes kind/tests Unit / integration tests merge-conflicts PR has merge conflicts size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: CEL Expression Support for Identity Fields in ImageValidatingPolicy

4 participants