Skip to content
Publishing signed bundles

Publishing signed bundles

github-sts verifies exactly one signature format: a standardized Sigstore bundle stored as an OCI 1.1 referrer. This page covers the versions that produce it and how to publish one to four common registries.

See Compatibility for what happens when a bundle is signed some other way.

For GitHub Container Registry with keyless signing, a coding agent can do these steps with the publish-policy-bundle skill. See Agent skills.

Minimum versions

Cosign

VersionResult
v3.0.0 and laterProduces the supported format by default. Recommended
v2.6.5Produces it only with --new-bundle-format=true. Tested and accepted
v2.x defaultsProduces the legacy sha256-<digest>.sig tag, which is not verified

Cosign v3 flipped --new-bundle-format to default true and deprecated the flag. The practical consequence is that the format written depends on the cosign major version, so pin cosign in the publishing pipeline. An unpinned cosign on $PATH, or an installer action’s default, silently decides whether the bundle you publish can be verified at all.

Registry

The registry must accept an OCI 1.1 image manifest carrying a subject field. The OCI Referrers API is preferred but not required: github-sts falls back to the referrers tag scheme when a registry does not implement the API.

RegistryMinimum versionNotes
GitHub Container RegistryNot applicable, hostedReferrers API supported for read and write
Harbor2.8 began OCI 1.1 distribution support, extended through 2.9Verified working on 2.15. Harbor’s UI may label a cosign v3 signature as an unknown accessory type; that is a display issue and does not affect verification
JFrog Artifactory7.90.1Referrers API added in this release
Sonatype Nexus Repository3.94.0OCI repositories, including Referrers API support, are new in this release

Registry version numbers move, and vendors backport. Treat the table as a starting point and confirm with the probe below rather than trusting it.

Check your registry before relying on it

Publish and sign one throwaway artifact, then read back what the registry actually stored. This takes a minute and answers the question the version table only estimates.

REF="<registry>/<path>/probe:$(date +%s)"
echo probe > probe.txt
crane append --oci-empty-base --new_layer probe.txt --new_tag "$REF"
DIGEST="$(crane digest "$REF")"
cosign sign --key cosign.key --yes "${REF%%:*}@${DIGEST}"

# Ask for referrers. A JSON index means the Referrers API is live.
curl -s -H "Accept: application/vnd.oci.image.index.v1+json" \
  "https://<registry>/v2/<path>/probe/referrers/${DIGEST}"

# A 404 above is fine. It means the tag fallback is in use, so look there:
curl -s "https://<registry>/v2/<path>/probe/manifests/${DIGEST/:/-}"

Either response must list a manifest whose layer media type is application/vnd.dev.sigstore.bundle.v0.3+json. If neither does, the registry did not store the signature in a form github-sts can find.

The descriptor in the tag-fallback index may report artifactType as application/vnd.oci.empty.v1+json rather than the bundle type. That is a known cosign limitation on registries without the Referrers API. github-sts fetches each referrer manifest rather than trusting the listing, so the signature is still found.

Build the bundle

Identical for every registry. Build with an explicit revision so the broker can match it against expected_policy_revision:

opa build --revision 42 -b policy -o bundle.tar.gz

crane append \
  --oci-empty-base \
  --new_layer bundle.tar.gz \
  --new_tag "<registry>/<path>/policy:v1.0.0"

# Sign the digest, never the tag: a tag can move between signing and pulling.
DIGEST="$(crane digest "<registry>/<path>/policy:v1.0.0")"

Publish and sign

Only authentication and the reference shape differ between registries. The signing command is the same everywhere.

GitHub Container Registry

echo "$GITHUB_TOKEN" | crane auth login ghcr.io --username "$GITHUB_ACTOR" --password-stdin
REPO="ghcr.io/<org>/<repository>"

In a workflow, sign keyless and skip key management entirely. The job needs id-token: write and packages: write:

- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
  with:
    cosign-release: v3.1.3   # pin it; the installer default is not a contract
- run: cosign sign --yes "${REPO}@${DIGEST}"

Configure the broker with the workflow identity that signed it:

cosign:
  certificate_identity_regexp: '^https://github\.com/<org>/<repo>/\.github/workflows/release\.yml@refs/tags/v.*$'
  certificate_oidc_issuer: https://token.actions.githubusercontent.com

Harbor

Use a robot account scoped to the project rather than a user login.

echo "$HARBOR_ROBOT_SECRET" | crane auth login harbor.example.com \
  --username 'robot$policy-publisher' --password-stdin
REPO="harbor.example.com/<project>/policy"

cosign sign --key cosign.key --yes "${REPO}@${DIGEST}"

Harbor is usually behind a private CA. github-sts has no CA configuration: it uses the system trust store, so the CA must be installed in the broker’s container. Without it the pull fails with an x509 error before verification runs, and the failure is counted as a pull failure rather than a signature one.

Sonatype Nexus Repository

Publish to an OCI repository, not a legacy Docker repository. The Referrers API arrived with the OCI repository format in 3.94.0.

echo "$NEXUS_PASSWORD" | crane auth login nexus.example.com \
  --username "$NEXUS_USER" --password-stdin
REPO="nexus.example.com/<oci-repository>/policy"

cosign sign --key cosign.key --yes "${REPO}@${DIGEST}"

If the repository is served on a non-default port, include it in the reference: nexus.example.com:8443/<oci-repository>/policy.

JFrog Artifactory

echo "$ARTIFACTORY_TOKEN" | crane auth login artifactory.example.com \
  --username "$ARTIFACTORY_USER" --password-stdin
REPO="artifactory.example.com/<docker-repository>/policy"

cosign sign --key cosign.key --yes "${REPO}@${DIGEST}"

Artifactory needs 7.90.1 or later for the Referrers API. On an older instance the signature is written through the tag fallback, which github-sts still reads.

Verify before pointing the broker at it

Check the signature with cosign first, so a verification failure in the broker is never ambiguous between a bad signature and a bad configuration:

cosign verify --key cosign.pub "${REPO}@${DIGEST}"

Then configure the broker against the same immutable digest:

bundles:
  - name: enterprise-baseline
    apps: []
    ref: oci://<registry>/<path>/policy@sha256:<digest>
    expected_policy_revision: "42"
    fail_mode: closed
    cosign:
      public_key_ref: /etc/github-sts/cosign.pub
    registry_auth:
      mode: basic
      username: policy-reader
      password_env: REGISTRY_PASSWORD

The credentials the broker uses must be able to list referrers, not only pull the manifest. A pull-only account produces discovery_failed rather than a missing-signature error. See Troubleshooting for the full error-code table.