Docker Image Signing and Attestation with Cosign

This article takes a detailed look at Cosign, one of several tools for creating and verifying Docker images and adding attestations such as build provenance.

Docker Image signing and attestation series

This article is part of a multi-part series:

Introduction

Cosign is one of several open-source tools developed in the Sigstore project. Sigstore is a graduated OpenSSF-backed project with the mission to provide free and effortless build provenance to as many artifact registries as possible. Thus, it is not limited to Docker/container images. For instance, Python packages can now be pushed to PyPI with Sigstore signatures (see announcement), Maven Central supports it (see announcement), and the macOS package manager Homebrew also adopted it (announcement).

Under Sigstore, various open source projects (other than Cosign) have been created, e.g. Fulcio & Rekor (that simplify “keyless signing”), Policy Controller (to verify signed images in Kubernetes), gitsign (to use keyless Sigstore signatures to sign Git commits), or Sigstore SDKs for various languages (so that you can build tools that sign or verify arbitrary artifacts).

Certificate-based vs. keyless signing

Certificate-based signing approaches (used by tools like Notation or Docker Content Trust) have various problems that Sigstore aims to solve:

  • Identity: as a consumer, you want to verify that the real (expected) publisher signed an artifact. Doing this via (long-lived) public-private key pairs is just a proxy, where possession of the private key is (supposedly) proving your identity. This proxy metaphor breaks down once a malicious actor (e.g. a hacker) stole a copy of the private key, and can henceforth claim to have the identity of the real publisher.
  • Key management: keeping private keys secure (so they cannot be lost or stolen) is difficult. Similarly, it’s difficult to make the public key easily accessible for users, while also protecting it from being manipulated by a malicious actor.
  • Key revocation: If the key pair is compromised, how do you distribute new keys in a way that convinces users of your legitimacy and that you’re not an attacker?

To solve these problems, Sigstore introduces “keyless” signing, which no longer needs long-lived key pair/signing certificates.

This is a simplified summary of the keyless signing flow:

  • When you run “cosign sign <artifact>”, Cosign generates a new public-private key pair, keeping it only in memory (not storing it on disk).
  • Cosign initiates an OIDC flow to get a (cryptographically verifiable) OIDC token (that attests the identity of whoever runs cosign) from one of the many OIDC issuers that Fulcio already trusts (see here for the complete list). This flow could either be completed by a human who manually logs into their Microsoft/GitHub/ etc. account, so the OIDC token would include the human’s email address and the URL of the chosen OIDC token issuer. Or it could be a service account token using fully automated means (without human interaction), e.g. a GitHub Actions runner could get such a token from GitHub’s own OIDC issuer (https://token.actions.githubusercontent.com), where the identity of the OIDC token is the full URL to the GitHub Actions workflow that runs “cosign sign”.
  • Cosign sends a Certificate Signing Request (that contains the OIDC token and the in-memory public key) to a Certificate Authority, Fulcio. Fulcio validates the OIDC token and returns a temporary signing certificate that is only valid for 10 minutes. Fulcio adds that certificate to its public immutable Certificate Transparency log. By default, Cosign uses a free-to-use “public good” instance of Fulcio, but you could also self-host Fulcio on your own servers, if necessary.
  • Cosign signs the artifact using the in-memory private key, and uploads the artifact’s digest, signature, and signing certificate to another public log, the Rekor ledger (again, using a public good instance by default, or you could self-host it).

As a consumer, when you run “cosign verify <artifact> --certificate-identity <email-address-or-URI> --certificate-oidc-issuer https://…”, Cosign performs these steps:

  • Retrieve the signature from the image registry
  • Verify that the signature is cryptographically valid
  • Check with Rekor that the signature is known there. If it is, also check with Fulcio that the certificate is known to Fulcio.
  • Verify that the identity (that is part of the signature’s certificate) matches the provided CLI arguments
    (--certificate-oidc-issuer, --certificate-identity)

In summary, these are the pros and cons of keyless signing:

  • Cons of keyless signing: single point of failure of signing infrastructure (Fulcio and Rekor). If that infrastructure is down, you cannot verify signatures. Self-hosting options are available but suffer from the same problem.
  • Pros of keyless signing: simplified key management and verification – the consumer only needs to configure two (human-readable) strings: the issuer URL and the identity (email address or URL)

Deep dive

Let’s examine the commands and the manifests or ledger entries they produce.

Signing images

To sign an image (using the keyless approach), run a command such as
“cosign sign [--recursive] registry.com/some/image@sha256:1234…”
where the digest points to either an image manifest (i.e., an image for a specific OS+CPU architecture) or an image index (which was created for multi-platform images; in this case, you should use Cosign’s --recursive flag to instruct cosign to recursively sign the platform-specific image manifests referenced by your provided image index).

As discussed above, if you run the command in an interactive terminal, cosign first asks you to log in with a supported SSO provider (e.g., Microsoft) in the browser, to retrieve your OIDC token. Or, if you run cosign in a CI/CD pipeline, it will auto-detect available issuers (such as GitHub or GitLab) and retrieve the OIDC token without user interaction.

Cosign then creates and pushes various manifests. Since Cosign v3.x, Cosign uses the Cosign Bundle Specification for signatures, and attaches them to registry.com/some/image@sha256:1234… via the referrer mechanism (which it also uses to discover signatures). I explain all details of the referrer mechanism in the What are “OCI References” box in my Notation article.

However, earlier Cosign versions used a (now deprecated) format described in the Cosign Signature Specification, pushing tags such as “sha256-<1234…>.sig” for signature discovery. As of Q4/2026, we are in a transition period. Some image vendors still produce the deprecated format and tags, while others already migrated to the new bundle format with referrers. The remainder of this article discusses the new bundle format used by default by Cosign v3.x.

Here is an example for the image ghcr.io/mshekow/package-version-check-mcp@sha256:2f65acaaf4bff2ce97b511675ae0f4183bd1135934561c8534647d3d3a37271c

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
      "digest": "sha256:eabaccc31da7dab44665e87c9bdf4149afdf04a60ce98c1dcec48e896bc8fafe",
      "size": 877
    }
  ]
}
Code language: JSON / JSON with Comments (json)

We see there is just one reference, a Sigstore Bundle, with digest eabac…

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "size": 2
  },
  "layers": [
    {
      "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
      "digest": "sha256:55444edbc5d4f6c001a9a2dd2082037a923c566ca294d873af4e8a07b5c69948",
      "size": 11122
    }
  ],
  "subject": {
    "mediaType": "application/vnd.oci.image.index.v1+json",
    "digest": "sha256:2f65acaaf4bff2ce97b511675ae0f4183bd1135934561c8534647d3d3a37271c",
    "size": 1609
  },
  "annotations": {
    "dev.sigstore.bundle.content": "dsse-envelope",
    "dev.sigstore.bundle.predicateType": "https://sigstore.dev/cosign/sign/v1",
    "org.opencontainers.image.created": "2026-10-02T17:55:09Z"
  }
}
Code language: JSON / JSON with Comments (json)

Relevant things to point out:

  • In this example, the layers array contains only one entry, but cosign supports signing an image multiple times, which would result in additional entries in layers.
  • The manifest of the config object (44136…) does not contain anything relevant; it’s practically empty.

Let’s look at the first layer’s contents (55444ed...):

{
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "dsseEnvelope": {
    "payload": "eyJfdHl...",
    "payloadType": "application/vnd.in-toto+json",
    "signatures": [
      {
        "sig": "MEYCIQDFj8+DVM8SHHo1lMFUX6lCEe+GhjaZ83NEP9l4f43oEwIhAPt7J8xAvWxEMnYeFeXeIx6kA8+43YltkezyFDmtiJJv"
      }
    ]
  },
  "verificationMaterial": {
    "certificate": {
      "rawBytes": "MIIHZDC..."
    },
    "timestampVerificationData": {
      "rfc3161Timestamps": [
        {
          "signedTimestamp": "MIICyTA..."
        }
      ]
    },
    "tlogEntries": [
      {
        "canonicalizedBody": "eyJhcGlWZ...",
        "inclusionPromise": {
          "signedEntryTimestamp": "MEUCIASJqVxs6zft8C0QEybWE9O780csdX2D7z6AFn5sulQhAiEAoNt6iqgKIU1C/7aaDxJykkZJtckAqxB68rd3zZXtwyg="
        },
        "inclusionProof": {
          "checkpoint": {
            "envelope": "rekor.sigstore.dev - 1193050959916656506\n2937630203\nefgbNZqI8Z2g/+B4IUY37ASpVAaBENpmP3YN/mwJ4b0=\n\n— rekor.sigstore.dev wNI9ajBFAiBDwdaCiHSsZAjEASwQwFF4/K1aFwsyMqUZRuPdhVmvmQIhAOO2tRkD9izL+gQ/Ke1M5DGu26pFgMHMc5hTogL8j5Am\n"
          },
          "hashes": [
            "DsDSrXBXw4ku4GayoPAZH/fe5X5nltOR1py2sJeo7zk=",
            "many-more-hashes"
          ],
          "logIndex": "2937630186",
          "rootHash": "efgbNZqI8Z2g/+B4IUY37ASpVAaBENpmP3YN/mwJ4b0=",
          "treeSize": "2937630203"
        },
        "integratedTime": "1790963709",
        "kindVersion": {
          "kind": "dsse",
          "version": "0.0.1"
        },
        "logId": {
          "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
        },
        "logIndex": "3059534448"
      }
    ]
  }
}
Code language: JSON / JSON with Comments (json)

The payload (line 4), once applying base64-decoding, looks as follows (indicating that Cosign’s image signature is basically an attestation of type https://sigstore.dev/cosign/sign/v1 with an empty predicate):

{
  "subject": [
    {
      "digest": {
        "sha256": "2f65acaaf4bff2ce97b511675ae0f4183bd1135934561c8534647d3d3a37271c"
      },
      "annotations": {}
    }
  ],
  "_type": "https://in-toto.io/Statement/v1",
  "predicateType": "https://sigstore.dev/cosign/sign/v1",
  "predicate": {}
}
Code language: JSON / JSON with Comments (json)

The sig in line 8 is the signature.

The certificate.rawBytes (line 14) shows the temporarily created signing certificate with various metadata. It looks as follows, when decoded with openssl, and the highlighted lines demonstrate important metadata:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            05:ab:78:90:2d:fb:50:c5:03:83:c7:af:2a:51:e5:32:6a:0e:3b:52
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: O = sigstore.dev, CN = sigstore-intermediate
        Validity
            Not Before: Oct  2 17:55:08 2026 GMT
            Not After : Oct  2 18:05:08 2026 GMT
        Subject: 
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:33:50:f3:e4:e9:bb:9a:24:fc:eb:e5:72:68:1b:
                    3f:34:cd:8c:f9:2b:e7:df:29:ec:64:58:2e:da:62:
                    44:28:8b:52:0c:80:63:3c:84:a4:78:54:10:aa:71:
                    55:5c:82:02:4d:35:16:bf:56:00:5c:d8:47:4d:b7:
                    55:58:bd:76:eb
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: 
                Code Signing
            X509v3 Subject Key Identifier: 
                CA:30:ED:99:49:B5:A4:EC:C0:CC:A3:43:E9:F3:C8:15:1D:DD:13:53
            X509v3 Authority Key Identifier: 
                DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
            X509v3 Subject Alternative Name: critical
                uri:https://github.com/MShekow/package-version-check-mcp/.github/workflows/ci-cd.yml@refs/tags/v1.2.34
            Fulcio Issuer: 
                https://token.actions.githubusercontent.com
            GitHub Workflow Trigger: 
                push
            GitHub Workflow SHA: 
                5e97c9d7c1be4f2db7d159590547056d24ff12d5
            GitHub Workflow Name: 
                CI-CD
            GitHub Workflow Repository: 
                MShekow/package-version-check-mcp
            GitHub Workflow Ref: 
                refs/tags/v1.2.34
            Fulcio Issuer (v2): 
                https://token.actions.githubusercontent.com
            Build Signer URI: 
                https://github.com/MShekow/package-version-check-mcp/.github/workflows/ci-cd.yml@refs/tags/v1.2.34
            Build Signer Digest: 
                5e97c9d7c1be4f2db7d159590547056d24ff12d5
            Runner Environment: 
                github-hosted
            Source Repository URI: 
                https://github.com/MShekow/package-version-check-mcp
            Source Repository Digest: 
                5e97c9d7c1be4f2db7d159590547056d24ff12d5
            Source Repository Ref: 
                refs/tags/v1.2.34
            Source Repository Identifier: 
                1140384463
            Source Repository Owner URI: 
                https://github.com/MShekow
            Source Repository Owner Identifier: 
                10126618
            Build Config URI: 
                https://github.com/MShekow/package-version-check-mcp/.github/workflows/ci-cd.yml@refs/tags/v1.2.34
            Build Config Digest: 
                5e97c9d7c1be4f2db7d159590547056d24ff12d5
            Build Trigger: 
                push
            Run Invocation URI: 
                https://github.com/MShekow/package-version-check-mcp/actions/runs/37043114494/attempts/1
            Source Repository Visibility At Signing: 
                public
            1.3.6.1.4.1.57264.1.23: 
                0c:04:70:79:70:69
            1.3.6.1.4.1.57264.1.24: 
                0c:37:72:65:70:6f:3a:4d:53:68:65:6b:6f:77:2f:70:61:63:6b:61:67:65:2d:76:65:72:73:69:6f:6e:2d:63:68:65:63:6b:2d:6d:63:70:3a:65:6e:76:69:72:6f:6e:6d:65:6e:74:3a:70:79:70:69
            CT Precertificate SCTs: 
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : DD:3D:30:6A:C6:C7:11:32:63:19:1E:1C:99:67:37:02:
                                A2:4A:5E:B8:DE:3C:AD:FF:87:8A:72:80:2F:29:EE:8E
                    Timestamp : Oct  2 17:55:08.947 2026 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:44:02:20:1C:21:EC:EE:BF:0F:7D:50:D0:F2:98:11:
                                DD:17:AE:2E:C2:79:2C:E7:C2:8B:A9:B2:4B:79:79:43:
                                46:17:C4:CD:02:20:64:EB:DA:15:78:AB:AB:7F:54:4F:
                                D0:20:97:36:F2:3E:D1:8B:70:C6:4B:CF:77:0B:9A:90:
                                EA:14:B5:24:CF:AE
    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:65:02:30:57:04:ac:15:bd:fc:35:c3:f9:f0:3f:b6:49:8e:
        60:e5:9a:79:69:8c:0b:54:5f:11:d5:9b:8c:d4:a7:8d:84:6b:
        2e:3c:ec:fe:f8:4e:c6:b2:64:e8:89:bf:fc:ed:a8:1f:02:31:
        00:af:dc:b7:ba:66:3d:02:69:a1:1e:28:b4:be:aa:34:f7:b8:
        f4:2a:50:33:10:49:c3:fe:84:bf:2a:fc:ca:33:b8:94:c5:18:
        cf:f0:23:1e:db:8f:f9:3e:ed:a4:95:1c:fd
Code language: JavaScript (javascript)

As you can see, Cosign did not just sign your image, but it also provided build provenance to some degree.

The logIndex (line 49) shows the Rekor Transparency Log ID, which you can look up in the browser (see here for the above example). Its content are essentially the same as the decoded signing certificate shown above.

Verifying images

To (manually) verify a signed image that was signed using keyless signing, use a command such as “cosign verify <registry.com/image:tag-or-digest> --certificate-identity email-or-url --certificate-oidc-issuer https://foobar.com”.

The verification only makes sense if you, the verifier, know who was supposed to sign the image. If you used incorrect values for --certificate-identity or --certificate-oidc-issuer, cosign prints an error message, which includes the identity (subject) and issuer of available signatures. If these values look correct to you, repeat the cosign verify command with the issuer and identity values from the error message.

Key-based signing & verification

Cosign also supports signing and verifying images using the “traditional” key-based approach, using a self-generated, long-living public-private key pair. This avoids the need for Rekor and Fulcio and can be appropriate for network-isolated environments, e.g., in an enterprise/corporate setting. This “offline signing” works as follows:

  • “cosign generate-key-pair” creates the files cosign.key and cosign.pub locally. Alternatively, you can also generate the key pair in a cloud-based KMS, e.g. Azure KeyVault, see the documentation for details
  • “cosign sign --key cosign.key <image-reference> --tlog-upload=false” signs the image, without making entries in Rekor’s transparency log
  • “cosign verify --key cosign.pub <image-reference> --insecure-ignore-tlog=true” verifies the image

Creating attestations

With the command “cosign attest --type <predicate-type> --predicate <path-to-attestation-file> <image-ref-with-tag-or-hash>” you tell Cosign to package the content of the attestation file (e.g., a CycloneDX-formatted SBOM, for the --predicate argument) in some wrapper manifests. Cosign also signs them cryptographically, and uploads them as Cosign bundle to the image registry. Cosign v2.x used to create a new tag named “sha256-<image-hash>.att” pointing to the attestation, while Cosign v3.x and newer no longer creates this tag, using the referrer API instead.

The file format that Cosign uses for the (wrapper) manifests is in-toto attestations. The “in-toto lingo” uses the term “Predicate” for the claim you want to attest (e.g., the SBOM). In-toto attestations can be generic, but the in-toto attestation framework also predefines various Predicate types; see here for a list. Cosign maps the parameter --type to these predefined Predicate types (--type can have the values slsaprovenance, slsaprovenance02, slsaprovenance1, link, spdx, spdxjson, cyclonedx, vuln, or openvex). You can also set --type to an arbitrary string, typically a URI such as “https://example.com/CodeReview/v1“.

The produced manifests have the same structure as described in section Signing images above. While an image signature‘s dsseEnvelope.payload (created by cosign sign ...) contained an empty predicate of type https://sigstore.dev/cosign/sign/v1, an attestation (created by cosign attest ...) contains sets the predicate and predicate type you defined in the cosign attest command.

Verifying attestations

To verify the authenticity of the signature of an attestation, run the command “cosign verify-attestation --type <predicate-type> --certificate-identity some@mail.com --certificate-oidc-issuer https://… <image-ref-with-digest>”

It’s also possible to validate the content (and Predicate type) using the CUE or Rego language. See here for examples. Alternatively, you can use the command
“cosign download attestation [--predicate-type] <image-ref-with-digest> > output.json”
to download the attestation.

Automated verification

To automatically verify image signatures, you have the following options:

  • At deployment time:
    • There are various open-source Kubernetes operators that can verify image signatures (and attestations) on-the-fly:
      • Kyverno (docs) supports verifying Cosign-created image signatures and attestations
      • FluxCD can verify Cosign-created image signatures (docs)
      • Ratify does not properly support Cosign’s new Bundle format (checked Ratify 1.4.6/v2.0.0-beta2)
      • Sigstore Policy Controller (docs) supports verifying Cosign-created image signatures and attestations
      • Connaisseur (docs) supports verifying Cosign-created image signatures, but as of 10/2026 it does not yet support the Cosign Bundle specification used by Cosign v3.x
    • Harbor (see docs) can be configured to refuse image pulls of unsigned images. Unfortunately, you cannot configure a trust policy in Harbor, meaning you cannot configure the specific signing identities you deem trustworthy. This makes the feature rather useless.
    • You can run “cosign verify” in CI/CD workflows. However, there is the “hen and egg” problem that you need to obtain a genuine cosign binary to begin with before you can use it. This could be as easy as hard-coding a curl download and hardcoding the binary’s corresponding checksum and verifying it. However, this would require regular maintenance. You can instead use a more elaborate “bootstrap approach”, as done by Cosign’s installer GitHub action; see here for details.
  • At build time: you can use multi-stage builds to have Notation verify your base image. See this example.

Conclusion

In the area of signing container images or adding attestations, Cosign is a very solid and easy-to-use solution. It has widespread adoption by other projects (that verify Cosign signatures), e.g., by Kubernetes Operators like Kyverno.

FYI: you can also use Cosign to sign ordinary files/blobs, see the documentation.

Leave a Comment