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 ghcr.io/kyverno/test-verify-image:signed -o tree
 2ghcr.io/kyverno/test-verify-image:signed
 3├── application/vnd.cncf.notary.signature
 4│   └── sha256:7f870420d92765b42cec0f71ee8e25bf39b692f64d95d6f6607e9e6e54300265
 5├── vulnerability-scan
 6│   └── sha256:f89cb7a0748c63a674d157ca84d725ff3ac09cc2d4aee9d0ec4315e0fe92a5fd
 7│       └── application/vnd.cncf.notary.signature
 8│           └── sha256:ec45844601244aa08ac750f44def3fd48ddacb736d26b83dde9f5d8ac646c2f3
 9└── sbom/cyclone-dx
10    └── sha256:8cad9bd6de426683424a204697dd48b55abcd6bb6b4930ad9d8ade99ae165414
11        └── application/vnd.cncf.notary.signature
12            └── sha256:61f3e42f017b72f4277c78a7a42ff2ad8f872811324cd984830dfaeb4030c322

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  validationFailureAction: Enforce
 7  webhookTimeoutSeconds: 30
 8  failurePolicy: Fail  
 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        attestors:
21        - count: 1
22          entries:
23          - certificates:
24              cert: |-
25                -----BEGIN CERTIFICATE-----
26                MIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
27                BAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG
28                Tm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx
29                MTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0
30                dGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3
31                DQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+
32                b+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL
33                hVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m
34                Iia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0
35                Vp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f
36                ETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG
37                A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G
38                CSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9
39                kYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8
40                Zq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF
41                ByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ
42                5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0
43                uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
44                -----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
├── vulnerability-scan
│  └── sha256:f89cb7a0748c63a674d157ca84d725ff3ac09cc2d4aee9d0ec4315e0fe92a5fd
│  └── application/vnd.cncf.notary.signature
│  └── sha256:ec45844601244aa08ac750f44def3fd48ddacb736d26b83dde9f5d8ac646c2f3
└── sbom/cyclone-dx
  └── sha256:8cad9bd6de426683424a204697dd48b55abcd6bb6b4930ad9d8ade99ae165414
  └── application/vnd.cncf.notary.signature
  └── sha256:61f3e42f017b72f4277c78a7a42ff2ad8f872811324cd984830dfaeb4030c322

This image has:

  1. A notary signature.
  2. A vulnerability scan report, signed using notary.
  3. A CycloneDX SBOM, 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  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: kyverno
21      verifyImages:
22      - type: Notary
23        imageReferences:
24          - "ghcr.io/kyverno/test-verify-image*"
25        attestations:
26          - type: sbom/cyclone-dx
27            attestors:
28            - entries:
29              - certificates: 
30                  cert: |-
31                    -----BEGIN CERTIFICATE-----
32                    MIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
33                    BAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG
34                    Tm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx
35                    MTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0
36                    dGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3
37                    DQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+
38                    b+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL
39                    hVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m
40                    Iia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0
41                    Vp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f
42                    ETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG
43                    A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G
44                    CSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9
45                    kYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8
46                    Zq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF
47                    ByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ
48                    5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0
49                    uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
50                    -----END CERTIFICATE-----                    
51            conditions:
52            - all:
53              - key: "{{ components[].licenses[].expression }}"
54                operator: AllIn
55                value: ["GPL-3.0"]
56              

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)