Skip to content

sigstore / Cosign⚓︎

sigstore is a Linux Foundation project that aims to provide public software signing and transparency to improve open source supply chain security. As part of the sigstore project, Cosign allows seamless container signing, verification and storage. You can read more about it here.

Connaisseur currently supports the elementary function of verifying Cosign-generated signatures based on the following types of keys:

We plan to expose further features of Cosign and sigstore in upcoming releases, so stay tuned!

Basic usage⚓︎

Getting started with Cosign is very well described in the docs. You can download Cosign from its GitHub repository. In short: After installation, a keypair is generated via:

cosign generate-key-pair

You will be prompted to set a password, after which a private (cosign.key) and public (cosign.pub) key are created. You can then use Cosign to sign a container image using:

# Here, ${IMAGE} is REPOSITORY/IMAGE_NAME:TAG
cosign sign --key cosign.key ${IMAGE}

The created signature can be verfied via:

cosign verify --key cosign.pub ${IMAGE}

To use Connaisseur with Cosign, configure a validator in charts/connaisseur/values.yaml with the generated public key (cosign.pub) as a trust root. The entry in .application.validators should look something like this (make sure to add your own public key to trust root default):

charts/connaisseur/values.yaml
- name: customvalidator
  type: cosign
  trustRoots:
  - name: default
    key: |  # YOUR PUBLIC KEY BELOW
      -----BEGIN PUBLIC KEY-----
      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvtc/qpHtx7iUUj+rRHR99a8mnGni
      qiGkmUb9YpWWTS4YwlvwdmMDiGzcsHiDOYz6f88u2hCRF5GUCvyiZAKrsA==
      -----END PUBLIC KEY-----

In .application.policy, add a pattern to match your public key to your own repository:

charts/connaisseur/values.yaml
- pattern: "docker.io/securesystemsengineering/testimage:co*"  # YOUR REPOSITORY
  validator: customvalidator

After installation, you are ready to verify your images against your public key:

helm install connaisseur helm --atomic --create-namespace --namespace connaisseur

A quick guide for installation and testing is available in getting started. In case you just use the default values for the validator and image policy given above, you are able to successfully validate our signed testimage:

kubectl run signed --image=docker.io/securesystemsengineering/testimage:co-signed

And compare this to the unsigned image:

kubectl run unsigned --image=docker.io/securesystemsengineering/testimage:co-unsigned

Or signed with a different key:

kubectl run altsigned --image=docker.io/securesystemsengineering/testimage:co-signed-alt

Configuration options⚓︎

.application.validators[*] in charts/connaisseur/values.yaml supports the following keys for Cosign (refer to basics for more information on default keys):

Key Default Required Description
name - ✔ See basics.
type - ✔ cosign; the validator type must be set to cosign.
trustRoots[*].name - ✔ See basics.
trustRoots[*].key - ✔ if not using keyless See basics. Public key from cosign.pub file or KMS URI. See additional notes below.
trustRoots[*].keyless.issuer - ✔ if not using a key or issuerRegex The OIDC provider URL which attests the identity.
trustRoots[*].keyless.subject - ✔ if not using a key or subjectRegex The identity that created the keyless signature. Usually an email address.
trustRoots[*].keyless.issuerRegex - ✔ if not using a key or issuer Regex for the OIDC provider URL which attests the identity.
trustRoots[*].keyless.subjectRegex - ✔ if not using a key or subject Regex of the identity that created the keyless signature. Usually an email address. When setting this, make sure you control all subject that can be matched. The pattern your.name@gmail.* also matches yourXname@gmail.com or your.name@gmail.attacker.com
host.rekor rekor.sigstore.dev - Rekor URL to use for validation against the transparency log (default sigstore instance is rekor.sigstore.dev). Setting host enforces successful transparency log check to pass verification. See additional notes below.
host.rekorPubkey Public key of rekor.sigstore.dev - Public key used to verify signature of log entry from Rekor.
host.fulcioCert Root and intermediate certificates belonging to fulcio.sigstore.dev - The root certificate belonging the Fulcio CA which is used to create keyless signatures.
host.ctLogPubkey Public key for the certificate transparency log provided by Sigstore - The public key needed for verifying Signed Certificate Timestamps (SCT). This will accept a single key.
auth. - - Authentication credentials for registries with restricted access (e.g. private registries or ratelimiting). See additional notes below.
auth.secretName - - Name of a Kubernetes secret in Connaisseur namespace that contains dockerconfigjson for registry authentication. See additional notes below.
auth.useKeychain false - When true, pass --k8s-keychain argument to cosign verify in order to use workload identities for authentication. See additional notes below.
cert - - A TLS certificate in PEM format for private registries with self-signed certificates.

.application.policy[*] in charts/connaisseur/values.yaml supports the following additional keys and modifications for sigstore/Cosign (refer to basics for more information on default keys):

Key Default Required Description
with.trustRoot - ✔ Setting the name of trust root to "*" enables verification of multiple trust roots. Refer to section on multi-signature verification for more information.
with.threshold - - Minimum number of signatures required in case with.trustRoot is set to "*". Refer to section on multi-signature verification for more information.
with.required [] - Array of required trust roots referenced by name in case with.trustRoot is set to "*". Refer to section on multi-signature verification for more information.
with.verifyInTransparencyLog true - Whether to include the verification using the Rekor tranparency log in the verification process. Refer to Tranparency log verification for more information.
with.verifySCT true - Whether to verify the signed certificate timestamps inside the transparency log.

Example⚓︎

charts/connaisseur/values.yaml
charts/connaisseur/values.yaml
application:
  validators:
  - name: myvalidator
    type: cosign
    trustRoots:
    - name: mykey
      key: |
        -----BEGIN PUBLIC KEY-----
        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvtc/qpHtx7iUUj+rRHR99a8mnGni
        qiGkmUb9YpWWTS4YwlvwdmMDiGzcsHiDOYz6f88u2hCRF5GUCvyiZAKrsA==
        -----END PUBLIC KEY-----

  policy:
  - pattern: "docker.io/securesystemsengineering/testimage:co-*"
    validator: myvalidator
    with:
      key: mykey

Additional notes⚓︎

Authentication⚓︎

When using a private registry for images and signature data, the credentials need to be provided to Connaisseur. There are two ways to do this.

dockerconfigjson⚓︎

Create a dockerconfigjson Kubernetes secret in the Connaisseur namespace and pass the secret name to Connaisseur as auth.secretName. The secret can for example be created directly from your local config.json (for docker this resides in ~/.docker/config.json):

kubectl create secret generic my-secret \
  --from-file=.dockerconfigjson=path/to/config.json \
  --type=kubernetes.io/dockerconfigjson \
  -n connaisseur

The secret can also be generated directly from supplied credentials (which may differ from your local config.json, using:

kubectl create secret docker-registry my-secret \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username='<your username>' \
  --docker-password='<your password>' \
  -n connaisseur

Info

At present, it seems to be necessary to suffix your registry server URL with /v1/. This may become unnecessary in the future.

In the above cases, the secret name in Connaisseur configuration would be secretName: my-secret. It is possible to provide one Kubernetes secret with a config.json for authentication to multiple private registries and referencing this in multiple validators.

K8s keychain⚓︎

Specification of auth.useKeychain: true in the validator configuration passes the --k8s-keychain to cosign when performing image validation. Thus, k8schain is used by cosign to pick up ambient registry credentials from the environment and for example use workload identities in case of common cloud providers.

For example, when validating against an ECR private repository, the credentials of an IAM user allowed to perform actions ecr:GetAuthorizationToken, ecr:BatchGetImage, and ecr:GetDownloadUrlForLayer could be added to the secret connaisseur-env-secrets:

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: connaisseur-env-secrets
  ...
data:
  AWS_ACCESS_KEY_ID: ***
  AWS_SECRET_ACCESS_KEY: ***
  ...

If useKeychain is set to true in the validator configuration, cosign will log into ECR at time of validation. See this cosign pull request for more details.

KMS Support⚓︎

Connaisseur supports Cosign's URI-based KMS integration to manage the signing and verification keys. Simply configure the trust root key value as the respective URI. In case of a Kubernetes secret, this would take the following form:

charts/connaisseur/values.yaml
- name: myvalidator
  type: cosign
  trustRoots:
  - name: mykey
    key: k8s://connaisseur/cosignkeys

For that specific case of a Kubernetes secret, make sure to place it in a suitable namespace and grant Connaisseur access to it1.

Most other KMS will require credentials for authentication that must be provided via environment variables. Such environment variables can be injected into Connaisseur via deployment.envs in charts/connaisseur/values.yaml, e.g.:

charts/connaisseur/values.yaml
  envs:
    VAULT_ADDR: myvault.com
    VAULT_TOKEN: secrettoken

Multi-signature verification⚓︎

Connaisseur can verify multiple signatures for a single image. It is possible to configure a threshold number and specific set of required valid signatures. This allows to implement several advanced use cases (and policies):

  • Five maintainers of a repository are able to sign a single derived image, however at least 3 signatures are required for the image to be valid.
  • In a CI pipeline, a container image is signed directly after pushing by the build job and at a later time by passing quality gates such as security scanners or integration tests, each with their own key (trust root). Validation requires all of these signatures for deployment to enforce integrity and quality gates.
  • A mixture of the above use cases whereby several specific trust roots are enforced (e.g. automation tools) and the overall number of signatures has to surpass a certain threshold (e.g. at least one of the testers admits).
  • Key rotation is possible by adding a new key as an additional key and require at least one valid signature.

Multi-signature verification is scoped to the trust roots specified within a referenced validator. Consider the following validator configuration:

charts/connaisseur/values.yaml
application:
  validators:
  - name: multicosigner
    type: cosign
    trustRoots:
    - name: alice
      key: |
        -----BEGIN PUBLIC KEY-----
        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEusIAt6EJ3YrTHdg2qkWVS0KuotWQ
        wHDtyaXlq7Nhj8279+1u/l5pZhXJPW8PnGRRLdO5NbsuM6aT7pOcP100uw==
        -----END PUBLIC KEY-----
    - name: bob
      key: |
        -----BEGIN PUBLIC KEY-----
        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01DasuXJ4rfzAEXsURSnbq4QzJ6o
        EJ2amYV/CBKqEhhl8fDESxsmbdqtBiZkDV2C3znIwV16SsJlRRYO+UrrAQ==
        -----END PUBLIC KEY-----
    - name: charlie
      key: |
        -----BEGIN PUBLIC KEY-----
        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEHBUYJVrH+aFYJPuryEkRyE6m0m4
        ANj+o/oW5fLRiEiXp0kbhkpLJR1LSwKYiX5Toxe3ePcuYpcWZn8Vqe3+oA==
        -----END PUBLIC KEY-----

The trust roots alice, bob, and charlie are all included for verification in case .application.policy[*].with.trustRoot is set to "*" (note that this is a special flag, not a real wildcard):

charts/connaisseur/values.yaml
- pattern: "*:*"
  validator: multicosigner
  with:
    trustRoot: "*"

As neither threshold nor required are specified, Connaisseur will require signatures of all trust roots (alice, bob, and charlie) and deny an image otherwise. If either threshold or required is specified, it takes precedence. For example, it is possible to configure a threshold number of required signatures via the threshold key:

charts/connaisseur/values.yaml
- pattern: "*:*"
  validator: multicosigner
  with:
    trustRoot: "*"
    threshold: 2

In this case, valid signatures of two or more out of the three trust roots are required for admittance. Using the required key, it is possible to enforce specific trusted roots:

charts/connaisseur/values.yaml
- pattern: "*:*"
  validator: multicosigner
  with:
    trustRoot: "*"
    required: ["alice", "bob"]

Now, only images with valid signatures of trust roots alice and bob are admitted. It is possible to combine threshold and required keys:

charts/connaisseur/values.yaml
- pattern: "*:*"
  validator: multicosigner
  with:
    trustRoot: "*"
    threshold: 3
    required: ["alice", "bob"]

Thus, at least 3 valid signatures are required and alice and bob must be among those.

Transparency log verification⚓︎

The sigstore project contains a transparency log called Rekor that provides an immutable, tamper-resistant ledger to record signed metadata to an immutable record. While it is possible to run your own instance, a public instance of Rekor is available at rekor.sigstore.dev. With Connaisseur it is possible to verify that a signature was added to the transparency log via the validators host.rekor key (see Cosign docs). When the host.rekor key is set, e.g. to rekor.sigstore.dev for the public instance, Connaisseur requires that a valid signature was added to the transparency log and deny an image otherwise. Furthermore, the host.rekor allows switching to private Rekor instances, e.g. for usage with keyless signatures. To disable this feature the with.verifyInTransparencyLog key can be set to false. This is useful for example if the signature was made without an upload to the transparency log in the first place.

Keyless signatures⚓︎

Keyless signatures are a feature of Sigstore that allows to sign container images without the need to manage a private key. Instead the signatures are bound to identities, attested by OIDC providers, and use ephemeral keys, short-lived certificates and a transparency log under the hood to provide similar security guarantees. Further information on this topic can be found here.

When using keyless signatures, the trustRoots[*].keyless field can be used to specify the issuer and subject of the keyless signature. The issuer is the OIDC provider that attests the identity and the subject is the identity that created the keyless signature, usually an email address. Both fields are also available as regular expressions. The following example shows how to configure a validator for keyless signatures:

charts/connaisseur/values.yaml
- name: keylessvalidator
  type: cosign
  trustRoots:
  - name: keyless
    keyless:
      issuerRegex: "github"
      subject: "philipp.belitz@securesystems.de

In case the signature was created using the Sigstore infrastructure, nothing else needs to be configured since Connaisseur will automatically retrieve all needed public keys and certificates from the Sigstore infrastructure. If the signature was created using a private infrastructure, the host.fulcioCert field can be used to specify the root certificate belonging to the Fulcio CA which is used to create the keyless signatures. The host.fulcioCert field should contain the root certificate in PEM format. The same applies to the host.ctLogPubkey field which can be used to specify the public key needed for verifying Signed Certificate Timestamps (SCT) and the host.rekorPubkey field which can be used to specify the public key used to verify the signature of log entries from Rekor.

charts/connaisseur/values.yaml
name: default
type: cosign
host:
  rekorPubkey: |
    -----BEGIN PUBLIC KEY-----
    ...
    -----END PUBLIC KEY-----
  ctLogPubkey: | 
    -----BEGIN PUBLIC KEY-----
    ...
    -----END PUBLIC KEY-----
  fulcioCert: |
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----
  ...

  1. The corresponding role and rolebinding should look similar to the following:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: connaisseur-kms-role
      namespace: connaisseur  # namespace of respective k8s secret, might have to change that
      labels:
        app.kubernetes.io/name: connaisseur
    rules:
    - apiGroups: ["*"]
      resources: ["secrets"]
      verbs: ["get"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: connaisseur-kms-rolebinding
      namespace: connaisseur  # namespace of respective k8s secret, might have to change that
      labels:
        app.kubernetes.io/name: connaisseur
    subjects:
    - kind: ServiceAccount
      name: connaisseur-serviceaccount
      namespace: connaisseur  # Connaisseur's namespace, might have to change that
    roleRef:
      kind: Role
      name: connaisseur-kms-role
      apiGroup: rbac.authorization.k8s.io
    
    Make sure to adjust it as needed.