Kubernetes

Kubernetes Secrets

Cosign can use keys stored in Kubernetes Secrets to sign and verify signatures. In order to generate a secret you have to pass cosign generate-key-pair a k8s://[NAMESPACE]/[NAME] URI specifying the namespace and secret name:

cosign generate-key-pair k8s://default/testsecret
Enter password for private key: ****
Enter again: ****
Successfully created secret testsecret in namespace default
Public key written to cosign.pub

After generating the key pair, cosign will store it in a Kubernetes secret using your current context. The secret will contain the private and public keys, as well as the password to decrypt the private key.

The secret has the following structure:

apiVersion: v1
kind: Secret
metadata:
  name: testsecret
  namespace: default
type: Opaque
data:
  cosign.key: LS0tLS1CRUdJTiBFTkNSWVBURUQgQ09TSUdOIFBSSVZBVEUgS0VZLS0tLS[...]==
  cosign.password: YWJjMTIz
  cosign.pub: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQo[...]==

When verifying an image signature using cosign verify, the key will be automatically decrypted using the password stored in the kubernetes secret under the cosign.password field.

Cosigned Admission Controller

The cosigned admission controller can be used to enforce policy on a Kubernetes cluster based on verifiable supply-chain metadata from cosign.

cosigned also resolves the image tags to ensure the image being ran is not different from when it was admitted.

See the installation instructions for more information.

This component is still actively under development!

Today, cosigned can automatically validate signatures on container images. Enforcement is configured on a per-namespace basis, and multiple keys are supported.

We're actively working on more features here.

Enable Cosigned Admission Controller for Namespaces

The cosigned admission controller will only validate resources in namespaces that have chosen to opt-in. This can be done by adding the label cosigned.sigstore.dev/include: "true" to the namespace resource.

kubectl label namespace my-secure-namespace cosigned.sigstore.dev/include=true

Admission of Images

An image is admitted after it has been validated against all ClusterImagePolicy that matched the digest of the image and that there was at least one valid signature or attestation obtained from the authorities provided in each of the matched ClusterImagePolicy. So each ClusterImagePolicy that matches is AND for admission, and within each ClusterImagePolicy authorities are ON.

See the Configuring Image Pattern for more information.

If no policy is matched against the image digest, the deprecated cosigned validation behavior will occur.

An example of an allowed admission would be:

  1. If the image matched against policy1 and policy3
  2. A valid signature or attestation was obtained for policy1 with at least one of the policy1 authorities
  3. A valid signature or attestation was obtained for policy3 with at least one of the policy3 authorities
  4. The image is admitted

An example of a denied admission would be:

  1. If the image matched against policy1 and policy2
  2. A valid signature or attestation was obtained for policy1 with at least one of the policy1 authorities
  3. No valid signature or attestation was obtained for policy2 with at least one of the policy2 authorities
  4. The image is not admitted

An example of no policy matched:

  1. If the image does not match against any policy
  2. Fallback to deprecated cosigned validation behavior
  3. Validation will be attempted against the secret defined under cosign.secretKeyRef.name during helm installation.
  4. If a valid signature or attestation is obtained, image is admitted
  5. If no valid signature or attestation is obtained, image is denied

Configuring Cosigned ClusterImagePolicy

cosigned supports validation against multiple ClusterImagePolicy kubernetes resources.

A policy is enforced when an image pattern for the policy is matched against the image being deployed.

Configuring Image Patterns

The ClusterImagePolicy specifies spec.images which specifies a list of glob matching patterns. These matching patterns will be matched against the image digest of PodSpec resources attempting to be deployed.

Glob uses golang filepath semantics for matching the images against. To make it easier to specify images, there are few defaults when an image is matched, namely:

  • If there is no host in the glob pattern index.docker.io is used for the host. This allows

users to specify commonly found images from Docker simply as myproject/nginx instead of inded.docker.io/myproject/nginx

  • If the image is specified without multiple path elements (so not separated by /), then library is defaulted. For example

specifying busybox will result in library/busybox. And combined with above, will result in match being made against index.docker.io/library/busybox.

A sample of a ClusterImagePolicy which matches against all images using glob:

apiVersion: cosigned.sigstore.dev/v1alpha1
kind: ClusterImagePolicy
metadata:
  name: image-policy
spec:
  images:
  - glob: "*"

Configuring key Authorities

When a policy is selected to be evaluated against the matched image, the authorities will be used to validate signatures and attestations. If at least one authority is satisfied and a signature is validated, the policy is validated.

Note: Currently, only ECDSA public keys are supported.

Authorities can be key specifications, for example:

spec:
  authorities:
    - key:
        data: |
          -----BEGIN PUBLIC KEY-----
          ...
          -----END PUBLIC KEY-----
    - key:
        secretRef:
          name: secretName
    - key:
        kms: KMSPATH

Each key authority can contain these properties:

  • key.data: specifies the plain text string of the public key
  • key.secretRef.name: specifies the secret location name in the same namespace where cosigned is installed.
    The first key value will be used in the secret.
  • key.kms: specifies the location for the public key. Supported formats include:
    • azurekms://[VAULT_NAME][VAULT_URI]/[KEY]
    • awskms://[ENDPOINT]/[ID/ALIAS/ARN]
    • gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]
    • hashivault://[KEY]

Configuring keyless Authorities

When a policy is selected to be evaluated against the matched image, the authorities will be used to validate signatures. If at least one authority is satisfied and a signature is validated, the policy is validated.

Authorities can be keyless specifications. For example:

spec:
  authorities:
    - keyless:
        url: https://fulcio.example.com
        ca-cert:
          data: Certificate Data
    - keyless:
        url: https://fulcio.example.com
        ca-cert:
          secretRef:
            name: secretName
    - keyless:
        identities:
          - issuer: https://accounts.google.com
            subject: .*@example.com
          - issuer: https://token.actions.githubusercontent.com
            subject: https://github.com/mycompany/*/.github/workflows/*@*

Each keyless authority can contain these properties:

  • keyless.url: specifies the Fulcio url
  • keyless.ca-cert: specifies ca-cert information for the keyless authority
    • secretRef.name: specifies the secret location name in the same namespace where cosigned is installed.
      The first key value will be used in the secret for the ca-cert.
    • data: specifies the inline certificate data
  • keyless.identities: Identity may contain an array of issuer and/or the subject found in the transparency log. Either field supports a regex.
    • issuer: specifies the issuer found in the transparency log. Regex patterns are supported.
    • subject: specifies the subject found in the transparency log. Regex patterns are supported.

Configuring Remote Signature Location

If signatures are located in a different repository, it can be specified along with the key or keyless definition. When no source is specified for the key, the expectation is that the signature is colocated with the image.

Note: By default, credentials used for the remote source repository are the ones provided in the PodSpec providing resource under imagePullSecrets.

To define a source, under the corresponding authorities node, source can be specified.

A sample of source specification for key and keyless:

spec:
  authorities:
    - key:
        data: |
          -----BEGIN PUBLIC KEY-----
          ...
          -----END PUBLIC KEY-----
      source:
        - oci: registry.example.com/project/signature-location
    - keyless:
        url: https://fulcio.example.com
      source:
        - oci: registry.example.com/project/signature-location

Configure SignaturePullSecrets

If the signatures / attestations are in a different repo or they use different PullSecrets, you can configure source to point to a secret which must live in the same namespace as cosigned webhook (by default cosign-system).

spec:
  authorities:
    - key:
        data: |
          -----BEGIN PUBLIC KEY-----
          ...
          -----END PUBLIC KEY-----
      source:
        - oci: registry.example.com/project/signature-location
    - keyless:
        url: https://fulcio.example.com
      source:
        - oci: registry.example.com/project/signature-location
          signaturePullSecrets:
          - name: mysecret

Note: The secret has to be in the format type: dockerconfigjson.

Configuring Transparency Log

TLog specifies the URL to a transparency log that holds signature and public key information.

When tlog key is not specified, the public rekor instance will be used.

spec:
  authorities:
    - keyless:
        url: https://fulcio.example.com
      tlog:
        url: https://rekor.example.com

Configuring policy that validates attestations

Just like with cosign CLI you can verify attestations (using verify-attestation), you can configure policies to validate that a particular attestation was signed by a trusted authority. You do this by using attestations array within an authorities section. For example, to configure that a custom predicate has to exist and is attested by the specified issuer and subject, and the actual Data section of the predicate matches the string foobar e2e test:

apiVersion: cosigned.sigstore.dev/v1alpha1
kind: ClusterImagePolicy
metadata:
  name: image-policy-keyless-with-attestations
spec:
  images:
  - glob: registry.local:5000/cosigned/demo*
  authorities:
  - name: verify custom attestation
    keyless:
      url: http://fulcio.fulcio-system.svc
      identities:
      - issuer: .*kubernetes.default.*
        subject: .*kubernetes.io/namespaces/default/serviceaccounts/default
    ctlog:
      url: http://rekor.rekor-system.svc
    attestations:
    - name: custom-match-predicate
      predicateType: custom
      policy:
        type: cue
        data: |
          predicateType: "cosign.sigstore.dev/attestation/v1"
          predicate: Data: "foobar e2e test"

policy is optional and if left out, only the existence of the attestation is verified.

Configuring policy at the ClusterImagePolicy level.

As discussed earlier, by specifying multiple ClusterImagePolicy creates an AND clause so that each ClusterImagePolicy must be satisfied for an admission, and having multiple authorities creates an OR clause so that any matching authority is considered a success, sometimes you may want more flexibility, for example, if you wanted to specify that at least 2 out of N signatures match, and for those you can create a single ClusterImagePolicy but craft a policy that then gets applied after a ClusterImagePolicy has been validated. Here's a bit more complex example that ties all the bits from above together. It requires there to be two attestations custom and vuln and also two signatures, one signed with a key and one keyless signature

apiVersion: cosigned.sigstore.dev/v1alpha1
kind: ClusterImagePolicy
metadata:
  name: image-policy-requires-two-signatures-two-attestations
spec:
  images:
  - glob: registry.local:5000/cosigned/demo*
  authorities:
  - name: keylessatt
    keyless:
      url: http://fulcio.fulcio-system.svc
    ctlog:
      url: http://rekor.rekor-system.svc
    attestations:
    - predicateType: custom
      name: customkeyless
      policy:
        type: cue
        data: |
          import "time"
          before: time.Parse(time.RFC3339, "2049-10-09T17:10:27Z")
          predicateType: "cosign.sigstore.dev/attestation/v1"
          predicate: {
            Data: "foobar e2e test"
            Timestamp: <before
          }
    - predicateType: vuln
      name: vulnkeyless
      policy:
        type: cue
        data: |
          import "time"
          before: time.Parse(time.RFC3339, "2022-04-15T17:10:27Z")
          after: time.Parse(time.RFC3339, "2022-03-09T17:10:27Z")
          predicateType: "cosign.sigstore.dev/attestation/vuln/v1"
          predicate: {
            invocation: {
              uri: "invocation.example.com/cosign-testing"
            }
            scanner: {
              uri: "fakescanner.example.com/cosign-testing"
            }
            metadata: {
              scanStartedOn: <before
              scanStartedOn: >after
              scanFinishedOn: <before
              scanFinishedOn: >after
            }
          }
  - name: keyatt
    key:
      data: |
        -----BEGIN PUBLIC KEY-----
        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOz9FcbJM/oOkC26Wfo9paG2tYGBL
        usDLHze93DzgLaAPDsyJrygpVnL9M6SOyfyXEsjpBTUu6uFZqHua8hwAlA==
        -----END PUBLIC KEY-----
    ctlog:
      url: http://rekor.rekor-system.svc
    attestations:
    - name: custom-match-predicate
      predicateType: custom
      policy:
        type: cue
        data: |
          predicateType: "cosign.sigstore.dev/attestation/v1"
          predicate: Data: "foobar key e2e test"
  - name: keylesssignature
    keyless:
      url: http://fulcio.fulcio-system.svc
    ctlog:
      url: http://rekor.rekor-system.svc
  - name: keysignature
    key:
      data: |
        -----BEGIN PUBLIC KEY-----
        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOz9FcbJM/oOkC26Wfo9paG2tYGBL
        usDLHze93DzgLaAPDsyJrygpVnL9M6SOyfyXEsjpBTUu6uFZqHua8hwAlA==
        -----END PUBLIC KEY-----
    ctlog:
      url: http://rekor.rekor-system.svc
  policy:
    type: cue
    data: |
      package sigstore
      import "struct"
      import "list"
      authorityMatches: {
        keyatt: {
          attestations: struct.MaxFields(1) & struct.MinFields(1)
        },
        keysignature: {
          signatures: list.MaxItems(1) & list.MinItems(1)
        },
        if (len(authorityMatches.keylessatt.attestations) < 2) {
          keylessattMinAttestations: 2
          keylessattMinAttestations: "Error"
        },
        keylesssignature: {
          signatures: list.MaxItems(1) & list.MinItems(1)
        }
      }

Deprecated Cosigned Validation Behavior

Note: This behavior is being deprecated in favor of using ClusterImagePolicy resources.

During the admission validation, if no ClusterImagePolicy is matched, the deprecated behavior will occur. Image digests will be validated against the public key secret defined by cosign.secretKeyRef.name during installation.

When installing cosigned through helm, cosign.secretKeyRef.name can be specified.

helm install cosigned -n cosign-system sigstore/cosigned --devel --set cosign.secretKeyRef.name=mysecret

The secret specified should contain the key cosign.pub and the public key data content.

Where cosign.pub is a file containing the public key, the kubernetes secret can be created with:

kubectl create secret generic mysecret -n cosign-system --from-file=cosign.pub=./cosign.pub

If the public key is able to validate a signature for the image digest, the admission controller will admit the image. If the public key is not able to validate a signature for the image digest, the admission controller will deny the image.

Edit this page on GitHub Updated at Sat, May 21, 2022