Skip to main content

Verify the distribution

You can independently verify the Stacklok-built container images you pull through the Replicated proxy (image-proxy.stacklok.com) without any access to Stacklok's internal registry or source repository. Verification works straight through the proxy with your license and the open source cosign tool.

All Stacklok images are signed with keyless Cosign signing through GitHub OIDC. There are no long-lived signing keys. Each signature is tied to the GitHub Actions workflow identity that produced it and recorded in Sigstore's public transparency log (Rekor). Each image also carries three signed attestations: an SPDX SBOM, SLSA build provenance, and an OpenVEX vulnerability assessment.

Replace these placeholders throughout:

  • <LICENSE_ID> - your Stacklok-issued license ID.
  • <YOUR_EMAIL> - the email on your customer record.
  • <IMAGE> - an image name from the table in Which images are signed (for example, operator).
  • <VERSION> - the platform version (the chart appVersion) you installed.
  • <NAMESPACE> and <SELECTOR> - the namespace and label selector for a running workload, when you read a digest from the cluster.
Verification relies on the proxy's Referrers behavior

Customers pull images through the Replicated proxy, not directly from a public registry. Cosign stores signatures and attestations as OCI referrers. The proxy returns a spec-compliant 404 on the OCI Referrers endpoint, so Cosign falls back to the referrers tag schema and verification works through the proxy with no re-signing. If you are on an older proxy deployment and verification fails with a 500 or Internal Server Error on a /referrers/ request, contact Stacklok.

Prerequisites

  • cosign v2.x or v3.x. This is the only tool you must install.
  • An OCI client you almost certainly already have, used only to log in and look up a digest: docker (or podman). oras or crane work too if you prefer. Cosign reuses this client's credential store, so there is no separate Cosign login.
  • Outbound HTTPS to image-proxy.stacklok.com, plus fulcio.sigstore.dev, rekor.sigstore.dev, and tuf-repo-cdn.sigstore.dev for Sigstore's roots.
  • A valid, non-expired license. The proxy authenticates every request against it. Your license is available from the Stacklok install portal at install.stacklok.com.

Step 1: Authenticate to the proxy

Log in once with whatever client you already use. The username is your customer email, and the password is the license ID (it is the bearer credential for the proxy, exactly as for chart pulls). All of these write to the same credential store (~/.docker/config.json or $REGISTRY_AUTH_FILE) that Cosign reads, so you do not run any Cosign-specific login.

# Docker (most common, since you already use it for image pulls).
# Paste <LICENSE_ID> at the password prompt.
docker login image-proxy.stacklok.com -u "<YOUR_EMAIL>"

# Podman
podman login image-proxy.stacklok.com -u "<YOUR_EMAIL>"

# oras
oras login image-proxy.stacklok.com -u "<YOUR_EMAIL>" --password-stdin <<<"<LICENSE_ID>"

# crane
crane auth login image-proxy.stacklok.com -u "<YOUR_EMAIL>" --password-stdin <<<"<LICENSE_ID>"
Keep the license ID out of your shell history

Pipe it on stdin instead of typing it inline:

echo "<LICENSE_ID>" | docker login image-proxy.stacklok.com \
-u "<YOUR_EMAIL>" --password-stdin

Step 2: Resolve the image digest

Verify by digest, never by a floating tag. Get the digest whichever way is easiest, using a tool you already have. The proxy path the examples use is the current one; the install portal prints the exact registry path for your release.

BASE=image-proxy.stacklok.com/proxy/stacklok-enterprise/ghcr.io/stacklok/stacklok-enterprise

# Best: read the digest of what you are ACTUALLY running, straight from the
# cluster (no extra tooling).
kubectl -n <NAMESPACE> get pod -l <SELECTOR> \
-o jsonpath='{.items[0].status.containerStatuses[0].imageID}'

# docker, without pulling (buildx ships with modern Docker)
docker buildx imagetools inspect "$BASE/<IMAGE>:<VERSION>" \
--format '{{json .Manifest.Digest}}' | tr -d '"'

# docker, plain (pulls the image, which you are doing anyway)
docker pull -q "$BASE/<IMAGE>:<VERSION>" >/dev/null \
&& docker inspect --format '{{index .RepoDigests 0}}' "$BASE/<IMAGE>:<VERSION>"

# oras
oras resolve "$BASE/<IMAGE>:<VERSION>"

# crane
crane digest "$BASE/<IMAGE>:<VERSION>"

Set DIGEST=sha256:... from whichever command you used.

note

Cosign can also verify a tag directly (cosign verify ... "$BASE/<IMAGE>:<VERSION>") and resolves the digest itself. Pinning to the digest you actually deployed (the kubectl line above) is the stronger check, since a tag can be repointed.

Step 3: Verify the image

The trust anchor is the signing identity: the signature was produced by a workflow under github.com/stacklok/stacklok-enterprise-platform/.github/workflows/, through GitHub's OIDC issuer. The regex below matches all three image-signing workflows.

IDENTITY='^https://github\.com/stacklok/stacklok-enterprise-platform/\.github/workflows/_release-(image|toolhive-cloud-ui|upstream-repackaged)\.yml@.*$'
ISSUER='https://token.actions.githubusercontent.com'
IMAGE="$BASE/<IMAGE>@${DIGEST}"

Verify the signature

cosign verify \
--certificate-identity-regexp="$IDENTITY" \
--certificate-oidc-issuer="$ISSUER" \
"$IMAGE"

A successful run prints Verification for ... -- followed by the validated claims, including the line "The code-signing certificate was verified using trusted certificate authority certificates", and a JSON array of the signed payloads.

Verify the SBOM attestation

Stacklok ships an SPDX SBOM as a signed in-toto attestation on every image:

cosign verify-attestation \
--type spdxjson \
--certificate-identity-regexp="$IDENTITY" \
--certificate-oidc-issuer="$ISSUER" \
"$IMAGE" \
| jq -r '.payload' | base64 -d \
| jq '{name: .predicate.name, spdxVersion: .predicate.spdxVersion, packages: (.predicate.packages | length)}'

The first lines confirm the attestation's signature, transparency-log, and certificate checks. The optional jq pipeline decodes the SPDX document and prints a summary (its name, SPDX version, and package count). Drop the jq filter to see the full SBOM.

Verify the SLSA build provenance

Every image carries SLSA build L3 provenance as a signed attestation (predicate type https://slsa.dev/provenance/v1):

cosign verify-attestation \
--type slsaprovenance1 \
--certificate-identity-regexp="$IDENTITY" \
--certificate-oidc-issuer="$ISSUER" \
"$IMAGE" \
| jq -r '.payload' | base64 -d \
| jq '.predicate.runDetails.builder.id, .predicate.buildDefinition.buildType'

--type slsaprovenance1 selects the SLSA provenance v1.0 schema; it is not a "level" selector. The build L3 property comes from how the provenance was produced (isolated GitHub Actions reusable workflows, non-forgeable OIDC-bound signing). Confirm it by checking that runDetails.builder.id is a trusted stacklok-enterprise-platform/.github/workflows/... reusable-workflow identity and that the certificate identity above matched.

Provenance availability

Cosign-verifiable provenance is present on releases built after Stacklok introduced it. For older releases, only a GitHub-native provenance attestation exists, which is not customer-verifiable because it requires read access to the private source repository. If cosign verify-attestation --type slsaprovenance1 reports no matching attestation, the image predates this and you can request the provenance from Stacklok.

Verify the OpenVEX attestation

Every image carries Stacklok's OpenVEX document as a signed attestation (predicate type https://openvex.dev/ns). It records the assessed status of known CVEs (for example, not_affected with a justification) so your scanner can suppress false positives:

cosign verify-attestation \
--type openvex \
--certificate-identity-regexp="$IDENTITY" \
--certificate-oidc-issuer="$ISSUER" \
"$IMAGE" \
| jq -r '.payload' | base64 -d \
| jq -r '.predicate.statements[] | "[\(.status)] \(.vulnerability.name) \(.justification // .impact_statement // "")"'

The first lines confirm the attestation's signature, transparency-log, and certificate checks. The jq pipeline lists each CVE and its assessment. To feed the document to a scanner that consumes OpenVEX (for example, trivy --vex), write the decoded predicate to a file:

cosign verify-attestation \
--type openvex \
--certificate-identity-regexp="$IDENTITY" \
--certificate-oidc-issuer="$ISSUER" \
"$IMAGE" \
| jq -r '.payload' | base64 -d | jq '.predicate' > vex.json

Inspect what is attached (optional)

If you have oras, it lists every artifact attached to the image (signature, SBOM, attestations) in one call:

oras discover "$IMAGE"

Which images are signed

Image (<IMAGE>)Signing workflowVerifiable
operator, proxyrunner, vmcp, registry-api, toolhive-enterprise_release-image.yml
ai-gateway-operator, ai-gateway-main-processor, ai-gateway-api-key-service_release-image.yml
cloud-ui_release-toolhive-cloud-ui.yml
ai-gateway-controller, ai-gateway-extproc_release-upstream-repackaged.yml
Third-party dependencies (Envoy Gateway, Envoy Ratelimit, Presidio, Replicated SDK)upstream, not re-signedn/a
Third-party dependency images

Third-party dependency images are served through the proxy for license-gated distribution but are not re-signed by Stacklok. Verify those against their original publishers' signatures if required. They are pulled by digest, so their content is pinned. Their proxy paths use the upstream host, for example .../proxy/stacklok-enterprise/docker.io/<UPSTREAM>/<IMAGE>.

Each third-party dependency plays a specific role in the platform:

DependencyRole in the stack
Envoy GatewayKubernetes Gateway API data plane that fronts the AI gateway
Envoy RatelimitRate-limiting service backing the AI gateway's token budget enforcement
PresidioMicrosoft Presidio engine for PII and PCI detection and redaction in prompts
Replicated SDKIn-cluster SDK that reports install status and license state to Replicated

Verify across all images

To check every Stacklok-signed image for a release in one pass:

BASE=image-proxy.stacklok.com/proxy/stacklok-enterprise/ghcr.io/stacklok/stacklok-enterprise
IDENTITY='^https://github\.com/stacklok/stacklok-enterprise-platform/\.github/workflows/_release-(image|toolhive-cloud-ui|upstream-repackaged)\.yml@.*$'
ISSUER='https://token.actions.githubusercontent.com'
VERSION='<VERSION>'

for IMAGE in operator proxyrunner vmcp registry-api toolhive-enterprise cloud-ui \
ai-gateway-operator ai-gateway-main-processor ai-gateway-api-key-service \
ai-gateway-controller ai-gateway-extproc; do
# Digest lookup via docker buildx (swap for `crane digest` or `oras resolve`).
DIGEST=$(docker buildx imagetools inspect "$BASE/$IMAGE:$VERSION" \
--format '{{json .Manifest.Digest}}' 2>/dev/null | tr -d '"')
[ -n "$DIGEST" ] || { echo "SKIP $IMAGE (no $VERSION)"; continue; }
if cosign verify --certificate-identity-regexp="$IDENTITY" \
--certificate-oidc-issuer="$ISSUER" "$BASE/$IMAGE@$DIGEST" >/dev/null 2>&1; then
echo "OK $IMAGE@$DIGEST"
else
echo "FAIL $IMAGE@$DIGEST"
fi
done

Next steps

Troubleshooting

no signatures found or 500 on a /referrers/ request

You are likely on an older proxy deployment. The proxy must return 404 on the OCI Referrers endpoint so Cosign falls back to the tag schema. Contact Stacklok.

401 Unauthorized on login or pull

Confirm <LICENSE_ID> is the password (not the username) and that your license is assigned to the channel serving the version. Expired licenses fail here.

certificate identity ... does not match

Cosign prints the actual certificate identity it found. Confirm it is a github.com/stacklok/stacklok-enterprise-platform/.github/workflows/... path signed by https://token.actions.githubusercontent.com. If it is not, do not trust the image and contact Stacklok.