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 chartappVersion) you installed.<NAMESPACE>and<SELECTOR>- the namespace and label selector for a running workload, when you read a digest from the cluster.
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
cosignv2.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(orpodman).orasorcranework 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, plusfulcio.sigstore.dev,rekor.sigstore.dev, andtuf-repo-cdn.sigstore.devfor 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>"
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.
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.
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 workflow | Verifiable |
|---|---|---|
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-signed | n/a |
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:
| Dependency | Role in the stack |
|---|---|
| Envoy Gateway | Kubernetes Gateway API data plane that fronts the AI gateway |
| Envoy Ratelimit | Rate-limiting service backing the AI gateway's token budget enforcement |
| Presidio | Microsoft Presidio engine for PII and PCI detection and redaction in prompts |
| Replicated SDK | In-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
- Configure platform identity to wire your identity provider to the platform components
- Configure policies to control client behavior across your organization
Related information
- Deploy the platform - the standard install, which pulls from Replicated at install time
- Install from a private registry (air-gapped) - mirror the chart and images into a registry you control
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.