Notary

Verify CNCF Notary format signatures using X.509 certificates.

Notary is a CNCF project that provides a specification and tooling for securing software supply chains.

The Notation CLI can be used to sign images and attestations in a CI/CD pipeline. A quick start guide providing a complete example of signing and verifying a container image using Notation can be found here.

The Notation CLI can also be used to inspect details of the container image signature.

 1notation inspect ghcr.io/kyverno/test-verify-image@sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
 2Inspecting all signatures for signed artifact
 3ghcr.io/kyverno/test-verify-image@sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
 4└── application/vnd.cncf.notary.signature
 5    └── sha256:7f870420d92765b42cec0f71ee8e25bf39b692f64d95d6f6607e9e6e54300265
 6        ├── media type: application/jose+json
 7        ├── signature algorithm: RSASSA-PSS-SHA-256
 8        ├── signed attributes
 9        │   ├── signingScheme: notary.x509
10        │   └── signingTime: Mon May 22 14:45:04 2023
11        ├── user defined attributes
12        │   └── (empty)
13        ├── unsigned attributes
14        │   └── signingAgent: Notation/1.0.0
15        ├── certificates
16        │   └── SHA256 fingerprint: da1f2d7d648dfacc7ebd59f98a9f35c753c331d80ca4280bb94060f4af4a5357
17        │       ├── issued to: CN=test,O=Notary,L=Seattle,ST=WA,C=US
18        │       ├── issued by: CN=test,O=Notary,L=Seattle,ST=WA,C=US
19        │       └── expiry: Thu May 19 21:15:18 2033
20        └── signed artifact
21            ├── media type: application/vnd.docker.distribution.manifest.v2+json
22            ├── digest: sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
23            └── size: 938

You can also use an OCI registry client to discover signatures and attestations for an image.

 1oras discover -o tree ghcr.io/kyverno/test-verify-image:signed
 2ghcr.io/kyverno/test-verify-image@sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
 3├── application/vnd.cncf.notary.signature
 4│   ├── sha256:7f870420d92765b42cec0f71ee8e25bf39b692f64d95d6f6607e9e6e54300265
 5│   └── sha256:f7d941ed9e93a1ff1d5dee3b091144a87dae1d73481d5be93aa65258a110c689
 6├── vulnerability-scan
 7│   └── sha256:f89cb7a0748c63a674d157ca84d725ff3ac09cc2d4aee9d0ec4315e0fe92a5fd
 8│       └── application/vnd.cncf.notary.signature
 9│           └── sha256:ec45844601244aa08ac750f44def3fd48ddacb736d26b83dde9f5d8ac646c2f3
10├── sbom/cyclone-dx
11│   └── sha256:8cad9bd6de426683424a204697dd48b55abcd6bb6b4930ad9d8ade99ae165414
12│       └── application/vnd.cncf.notary.signature
13│           └── sha256:61f3e42f017b72f4277c78a7a42ff2ad8f872811324cd984830dfaeb4030c322
14├── application/vnd.cyclonedx+json
15│   └── sha256:aa886b475b431a37baa0e803765a9212f0accece0b82a131ebafd43ea78fa1f8
16│       └── application/vnd.cncf.notary.signature
17│           ├── sha256:00c5f96577878d79b545d424884886c37e270fac5996f17330d77a01a96801eb
18│           └── sha256:f3dc4687f5654ea8c2bc8da4e831d22a067298e8651fb59d55565dee58e94e2d
19├── cyclonedx/vex
20│   └── sha256:c058f08c9103bb676fcd0b98e41face2436e0a16f3d1c8255797b916ab5daa8a
21│       └── application/vnd.cncf.notary.signature
22│           └── sha256:79edc8936a4fb8758b9cb2b8603a1c7903f53261c425efb0cd85b09715eb6dfa
23└── trivy/scan
24    └── sha256:a75ac963617462fdfe6a3847d17e5519465dfb069f92870050cce5269e7cbd7b
25        └── application/vnd.cncf.notary.signature
26            └── sha256:d1e2b2ba837c164c282cf389594791a190df872cf7712b4d91aa10a3520a8460

Verifying Image Signatures

The following policy checks whether an image is signed with a valid X.509 key that matches the provided public certificate.

 1apiVersion: kyverno.io/v2beta1
 2kind: ClusterPolicy
 3metadata:
 4  name: check-image-notary
 5spec:
 6  webhookConfiguration:
 7    failurePolicy: Fail
 8    timeoutSeconds: 30
 9  rules:
10    - name: verify-signature-notary
11      match:
12        any:
13        - resources:
14            kinds:
15              - Pod
16      verifyImages:
17      - type: Notary
18        imageReferences:
19        - "ghcr.io/kyverno/test-verify-image*"
20        failureAction: Enforce
21        attestors:
22        - count: 1
23          entries:
24          - certificates:
25              cert: |-
26                -----BEGIN CERTIFICATE-----
27                MIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
28                BAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG
29                Tm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx
30                MTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0
31                dGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3
32                DQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+
33                b+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL
34                hVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m
35                Iia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0
36                Vp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f
37                ETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG
38                A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G
39                CSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9
40                kYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8
41                Zq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF
42                ByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ
43                5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0
44                uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
45                -----END CERTIFICATE-----                

With this policy configured, Kyverno will verify matching container image signatures and only allow the pod to be configured if the signatures are valid.

1kubectl run test --image=ghcr.io/kyverno/test-verify-image:signed --dry-run=server
2pod/test created (server dry run)

Kyverno will also mutate the pod to replace the image tag with its digest.

1kubectl run test --image=ghcr.io/kyverno/test-verify-image:signed --dry-run=server -o yaml | grep "image: "
2  - image: ghcr.io/kyverno/test-verify-image:signed@sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105

Attempting to run a pod with an unsigned image will be blocked.

 1kubectl run test --image=ghcr.io/kyverno/test-verify-image:unsigned --dry-run=server
 2Error from server: admission webhook "mutate.kyverno.svc-fail" denied the request:
 3
 4resource Pod/default/test was blocked due to the following policies
 5
 6check-image-notary:
 7  verify-signature-notary: 'failed to verify image ghcr.io/kyverno/test-verify-image:unsigned:
 8    .attestors[0].entries[0]: failed to verify ghcr.io/kyverno/test-verify-image@sha256:74a98f0e4d750c9052f092a7f7a72de7b20f94f176a490088f7a744c76c53ea5:
 9    no signature is associated with "ghcr.io/kyverno/test-verify-image@sha256:74a98f0e4d750c9052f092a7f7a72de7b20f94f176a490088f7a744c76c53ea5",
10    make sure the image was signed successfully'

Verifying Image Attestations

Consider the following image: ghcr.io/kyverno/test-verify-image:signed

ghcr.io/kyverno/test-verify-image:signed
├── application/vnd.cncf.notary.signature
│   ├── sha256:7f870420d92765b42cec0f71ee8e25bf39b692f64d95d6f6607e9e6e54300265
│   └── sha256:f7d941ed9e93a1ff1d5dee3b091144a87dae1d73481d5be93aa65258a110c689
├── vulnerability-scan
│   └── sha256:f89cb7a0748c63a674d157ca84d725ff3ac09cc2d4aee9d0ec4315e0fe92a5fd
│       └── application/vnd.cncf.notary.signature
│           └── sha256:ec45844601244aa08ac750f44def3fd48ddacb736d26b83dde9f5d8ac646c2f3
├── sbom/cyclone-dx
│   └── sha256:8cad9bd6de426683424a204697dd48b55abcd6bb6b4930ad9d8ade99ae165414
│       └── application/vnd.cncf.notary.signature
│           └── sha256:61f3e42f017b72f4277c78a7a42ff2ad8f872811324cd984830dfaeb4030c322
├── application/vnd.cyclonedx+json
│   └── sha256:aa886b475b431a37baa0e803765a9212f0accece0b82a131ebafd43ea78fa1f8
│       └── application/vnd.cncf.notary.signature
│           ├── sha256:00c5f96577878d79b545d424884886c37e270fac5996f17330d77a01a96801eb
│           └── sha256:f3dc4687f5654ea8c2bc8da4e831d22a067298e8651fb59d55565dee58e94e2d
├── cyclonedx/vex
│   └── sha256:c058f08c9103bb676fcd0b98e41face2436e0a16f3d1c8255797b916ab5daa8a
│       └── application/vnd.cncf.notary.signature
│           └── sha256:79edc8936a4fb8758b9cb2b8603a1c7903f53261c425efb0cd85b09715eb6dfa
└── trivy/scan
    └── sha256:a75ac963617462fdfe6a3847d17e5519465dfb069f92870050cce5269e7cbd7b
        └── application/vnd.cncf.notary.signature
            └── sha256:d1e2b2ba837c164c282cf389594791a190df872cf7712b4d91aa10a3520a8460

This image has:

  1. A notary signature.
  2. A vulnerability scan report, signed using notary.
  3. A CycloneDX SBOM, signed using notary.
  4. A CycloneDX VEX report, signed using notary.
  5. A Trivy scan report, signed using notary.

This policy checks the signature in the repo ghcr.io/kyverno/test-verify-image and ensures that it has been signed by verifying its signature against the provided certificates:

 1apiVersion: kyverno.io/v1
 2kind: ClusterPolicy
 3metadata:
 4  name: check-image-attestation
 5spec:
 6  webhookConfiguration:
 7    failurePolicy: Fail
 8    timeoutSeconds: 30
 9  rules:
10    - name: verify-attestation-notary
11      match:
12        any:
13        - resources:
14            kinds:
15              - Pod
16      context:
17      - name: keys
18        configMap:
19          name: keys
20          namespace: kyverno
21      verifyImages:
22      - type: Notary
23        imageReferences:
24          - "ghcr.io/kyverno/test-verify-image*"
25        failureAction: Enforce
26        attestations:
27          - type: sbom/cyclone-dx
28            attestors:
29            - entries:
30              - certificates: 
31                  cert: |-
32                    -----BEGIN CERTIFICATE-----
33                    MIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
34                    BAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG
35                    Tm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx
36                    MTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0
37                    dGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3
38                    DQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+
39                    b+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL
40                    hVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m
41                    Iia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0
42                    Vp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f
43                    ETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG
44                    A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G
45                    CSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9
46                    kYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8
47                    Zq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF
48                    ByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ
49                    5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0
50                    uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
51                    -----END CERTIFICATE-----                    
52            conditions:
53            - all:
54              - key: "{{ components[].licenses[].expression }}"
55                operator: AllIn
56                value: ["GPL-3.0"]
57              

After this policy is applied, Kyverno will verify the signature on the sbom/cyclone-dx attestation and check if the license version of all the components in the SBOM is GPL-3.0.

1kubectl run test --image=ghcr.io/kyverno/test-verify-image:signed --dry-run=server
2pod/test created (server dry run)

Validation across multiple image attestations

Consider the image: ghcr.io/kyverno/test-verify-image:signed which image has:

  1. A notary signature.
  2. A vulnerability scan report, signed using notary.
  3. A CycloneDX VEX report, signed using notary.

This policy checks:

  1. The signature in the repo ghcr.io/kyverno/test-verify-image
  2. Ensures that it has a vulnerability scan report of type trivy/vulnerability, and a CycloneDX VEX report of type vex/cyclone-dx, both are signed using the given certificate.
  3. All the vulnerabilities found in the trivy scan report should be allowed in the vex report.
 1apiVersion: kyverno.io/v1
 2kind: ClusterPolicy
 3metadata:
 4  name: check-image-attestation
 5spec:
 6  validationFailureAction: Enforce
 7  webhookTimeoutSeconds: 30
 8  failurePolicy: Fail  
 9  rules:
10    - name: verify-attestation-notary
11      match:
12        any:
13        - resources:
14            kinds:
15              - Pod
16      context:
17      - name: keys
18        configMap:
19          name: keys
20          namespace: notary-verify-attestation
21      verifyImages:
22      - type: Notary
23        imageReferences:
24          - "ghcr.io/kyverno/test-verify-image*"
25        attestations:
26          - type: trivy/vulnerability
27            name: trivy
28            attestors:
29            - entries:
30              - certificates: 
31                  cert: |-
32                    -----BEGIN CERTIFICATE-----
33                    MIIDmDCCAoCgAwIBAgIUCntgF4FftePAhEa6nZTsu/NMT3cwDQYJKoZIhvcNAQEL
34                    BQAwTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0dGxl
35                    MQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwHhcNMjQwNjEwMTYzMTQ2
36                    WhcNMzQwNjA4MTYzMTQ2WjBMMQswCQYDVQQGEwJVUzELMAkGA1UECAwCV0ExEDAO
37                    BgNVBAcMB1NlYXR0bGUxDzANBgNVBAoMBk5vdGFyeTENMAsGA1UEAwwEdGVzdDCC
38                    ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkEGqbILiWye6C1Jz+jwwDY
39                    k/rovpXzxS+EQDvfj/YKvx37Kr4cjboJORu3wtzICWhPUtVWZ21ShfjerKgNq0iB
40                    mrlF4cqz2KcOfuUT3XBglH/NwhEAqOrGPQrMsoQEFWgnilr0RTc+j4vDnkdkcTj2
41                    K/qPhQHRAeb97TdvFCqcZfAGqiOVUqzDGxd2INz/fJd4/nYRX3LJBn9pUGxqRwZV
42                    ElP5B/aCBjJDdh6tAElT5aDnLGAB+3+W2YwG342ELyAl2ILpbSRUpKLNAfKEd7Nj
43                    1moIl4or5AIlTkgewZ/AK68HPFJEV3SwNbzkgAC+/mLVCD8tqu0o0ziyIUJtoQMC
44                    AwEAAaNyMHAwHQYDVR0OBBYEFFTIzCppwv0vZnAVmETPm1CfMdcYMB8GA1UdIwQY
45                    MBaAFFTIzCppwv0vZnAVmETPm1CfMdcYMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD
46                    AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQB8/vfP
47                    /TQ3X80JEZDsttdvd9NLm08bTJ/T+nh0DIiV10aHymQT9/u+iahfm1+7mj+uv8LS
48                    Y63LepQCX5p9SoFzt513pbNYXMBbRrOKpth3DD49IPL2Gce86AFGydfrakd86CL1
49                    9MhFeWhtRf0KndyUX8J2s7jbpoN8HrN4/wZygiEqbQWZG8YtIZ9EewmoVMYirQqH
50                    EvW93NcgmjiELuhjndcT/kHjhf8fUAgSuxiPIy6ern02fJjw40KzgiKNvxMoI9su
51                    G2zu6gXmxkw+x0SMe9kX+Rg4hCIjTUM7dc66XL5LcTp4S5YEZNVC40/FgTIZoK0e
52                    r1dC2/Y1SmmrIoA1
53                    -----END CERTIFICATE-----                    
54          - type: vex/cyclone-dx
55            name: vex
56            attestors:
57            - entries:
58              - certificates: 
59                  cert: |-
60                    -----BEGIN CERTIFICATE-----
61                    MIIDmDCCAoCgAwIBAgIUCntgF4FftePAhEa6nZTsu/NMT3cwDQYJKoZIhvcNAQEL
62                    BQAwTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0dGxl
63                    MQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwHhcNMjQwNjEwMTYzMTQ2
64                    WhcNMzQwNjA4MTYzMTQ2WjBMMQswCQYDVQQGEwJVUzELMAkGA1UECAwCV0ExEDAO
65                    BgNVBAcMB1NlYXR0bGUxDzANBgNVBAoMBk5vdGFyeTENMAsGA1UEAwwEdGVzdDCC
66                    ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkEGqbILiWye6C1Jz+jwwDY
67                    k/rovpXzxS+EQDvfj/YKvx37Kr4cjboJORu3wtzICWhPUtVWZ21ShfjerKgNq0iB
68                    mrlF4cqz2KcOfuUT3XBglH/NwhEAqOrGPQrMsoQEFWgnilr0RTc+j4vDnkdkcTj2
69                    K/qPhQHRAeb97TdvFCqcZfAGqiOVUqzDGxd2INz/fJd4/nYRX3LJBn9pUGxqRwZV
70                    ElP5B/aCBjJDdh6tAElT5aDnLGAB+3+W2YwG342ELyAl2ILpbSRUpKLNAfKEd7Nj
71                    1moIl4or5AIlTkgewZ/AK68HPFJEV3SwNbzkgAC+/mLVCD8tqu0o0ziyIUJtoQMC
72                    AwEAAaNyMHAwHQYDVR0OBBYEFFTIzCppwv0vZnAVmETPm1CfMdcYMB8GA1UdIwQY
73                    MBaAFFTIzCppwv0vZnAVmETPm1CfMdcYMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD
74                    AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQB8/vfP
75                    /TQ3X80JEZDsttdvd9NLm08bTJ/T+nh0DIiV10aHymQT9/u+iahfm1+7mj+uv8LS
76                    Y63LepQCX5p9SoFzt513pbNYXMBbRrOKpth3DD49IPL2Gce86AFGydfrakd86CL1
77                    9MhFeWhtRf0KndyUX8J2s7jbpoN8HrN4/wZygiEqbQWZG8YtIZ9EewmoVMYirQqH
78                    EvW93NcgmjiELuhjndcT/kHjhf8fUAgSuxiPIy6ern02fJjw40KzgiKNvxMoI9su
79                    G2zu6gXmxkw+x0SMe9kX+Rg4hCIjTUM7dc66XL5LcTp4S5YEZNVC40/FgTIZoK0e
80                    r1dC2/Y1SmmrIoA1
81                    -----END CERTIFICATE-----                    
82        validate:
83          deny:
84            conditions:
85              any:
86              - key: '{{ trivy.Vulnerabilities[*].VulnerabilityID }}'
87                operator: AnyNotIn
88                value: '{{ vex.vulnerabilities[*].id }}'
89          message: All vulnerabilities in trivy and vex should be same

After this policy is applied, Kyverno will verify the signatures in the image and the attestations and then evaluate the validate deny condition which checks all the vulneribilities in trivy report are there in vex report.

1kubectl run test --image=ghcr.io/kyverno/test-verify-image:signed --dry-run=server
2pod/test created (server dry run)