Skip to content

Release verification ​

How a consumer — or you, during an incident — proves that a published TeslaSync image is the artifact this repository built, and nothing else.

Policy: ops/release/supply-chain.yaml Gate: go run ./cmd/ops-gate -check supply-chain Producer: .github/workflows/release.yml

Published package lifetime and protected rollback versions are covered by the package retention policy.

What every published image carries ​

ArtifactProduced byProves
Cosign signature (keyless, GitHub OIDC)cosign signThese bytes were signed by a workflow in this repository.
CycloneDX SBOM attestationsyft + cosign attestExactly which Go modules, OS packages, and JS dependencies landed in the final layer.
SPDX SBOM (artifact)syftThe same inventory in the SPDX format, for tooling that needs it.
SLSA build provenanceactions/attest-build-provenanceWhich workflow file, commit, and runner produced these bytes.
Vulnerability statustrivy + the vulnerability-report jobThe fixable CVE counts at build time, published in the release notes.

All of them are bound to the image digest, never to a tag. Re-tagging cannot transplant an attestation onto different bytes.

Verify a release ​

The release workflow creates a draft, attaches the Windows installer, unsigned MSIX, and debug Android APK, then publishes it. This order is required when GitHub release immutability is enabled: assets cannot be added after publication, including to branch prereleases. Check that all three downloads appear on the published release before using them. A failed, published prerelease with missing assets cannot be repaired in place; rerun from a new commit to create a new tag.

Set the version you are checking:

bash
IMAGE=ghcr.io/ev-dev-labs/teslasync-api
VERSION=1.4.2

1. Signature ​

bash
cosign verify "${IMAGE}:${VERSION}" \
  --certificate-identity-regexp="github.com/ev-dev-labs/teslasync" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com"

A failure here means the image was not signed by this repository's release workflow. Stop; do not deploy it.

2. SBOM ​

bash
cosign verify-attestation \
  --type cyclonedx \
  --certificate-identity-regexp="github.com/ev-dev-labs/teslasync" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  "${IMAGE}:${VERSION}" \
  | jq -r '.payload' | base64 -d | jq '.predicate' > sbom.cdx.json

# e.g. "is the vulnerable version of X in this image?"
jq -r '.components[] | "\(.name) \(.version)"' sbom.cdx.json | grep -i some-package

This is the fastest way to answer "are we affected?" when a CVE lands — faster than rebuilding, and it reflects what actually shipped rather than what go.mod currently says.

3. Build provenance ​

bash
gh attestation verify "oci://${IMAGE}:${VERSION}" --repo ev-dev-labs/teslasync

The signature says someone in this repo signed it. Provenance says which workflow, at which commit, on which runner. Check the reported workflow path is .github/workflows/release.yml and the commit is one you recognise.

4. Vulnerability status ​

Read the 🛡️ Vulnerability status table in the GitHub release notes, or download the vulnerability-status-<version> artifact from the release run.

Policy (from ops/release/supply-chain.yaml):

  • Fails the release: any fixable CRITICAL.
  • Reported, not blocking: HIGH, MEDIUM, and every unfixable finding.

ignore_unfixed: true is deliberate. An unfixable CRITICAL in a base image would otherwise block every release indefinitely while providing no path to remediation — so it is surfaced and tracked instead of silently jamming the pipeline.

Artifact promotion — the bytes you verify are the bytes we scanned ​

Ordering a vulnerability gate before the push is necessary but not sufficient. An earlier version of this workflow had exactly that ordering — build-scan → vulnerability-report → docker — and still published bytes that were never scanned, because the docker job re-built the image from source with push: true instead of promoting the artifact the scan had assessed.

Two builds are not one artifact. An unpinned base tag, an evicted BuildKit cache entry, or any non-reproducible layer makes the pushed image differ from the scanned one. The cosign signature and the SLSA provenance would then describe an image nobody assessed — which is worse than no attestation at all, because it reads as assurance.

The pipeline now promotes one artifact end to end:

  1. build-scan builds once with push: false and outputs: type=docker,dest=/tmp/promote/<image>.tar. Nothing touches a registry.
  2. It records the archive's config digest (which commits to every layer diff_id) and the archive's SHA-256, then scans the archive itself with trivy --input.
  3. The archive is uploaded as image-<image>-<version> — the image name and version in the artifact name keep parallel matrix legs isolated.
  4. vulnerability-report aggregates every leg and produces the single verdict. A fixable CRITICAL stops the run here, before any public tag exists.
  5. docker downloads that artifact, re-checks the archive SHA-256, and crane pushes it. :latest is a tag on the same manifest, never a second build.
  6. It then asserts DIGEST CONTINUITY: the archive's config digest must equal the config digest of the pushed manifest. If they ever diverge the job fails before signing.
  7. cosign, the SBOM attestation, and the provenance subject all bind to steps.promote.outputs.digest — the digest that was actually pushed.

Two gates keep this honest:

  • go run ./cmd/ops-gate -check workflows parses the workflow's action steps and fails if the scan job sets with.push: true, if a publish job invokes a build action at all, if the uploaded and downloaded artifact names differ, or if the continuity marker is missing.
  • go run ./cmd/ops-gate -check supply-chain requires crane push and DIGEST CONTINUITY to be present in the release run blocks.

Immutability ​

Everything the release workflow pulls is pinned:

  • Third-party actions are pinned to a full 40-character commit SHA with a trailing # vX.Y.Z comment so the pin stays reviewable.
  • Scanner container images are pinned by @sha256: digest.

go run ./cmd/ops-gate -check supply-chain fails on any mutable reference. Images this workflow builds are exempt from digest pinning for the obvious reason: their digest does not exist until the build produces it.

The separate scripts/check-security-workflow-pins.go owns .github/workflows/security.yml. The two gates are deliberately independent so neither can be weakened by a change to the other.

Exceptions ​

A vulnerability exception must carry an ID, a reason, and an expiry date:

yaml
vulnerability_policy:
  exceptions:
    - id: CVE-2026-12345
      reason: No upstream fix; not reachable from any request path.
      expires: "2026-12-31"

The gate fails on an expired entry, so an exception cannot quietly become permanent.

When verification fails ​

  1. Do not deploy the image.
  2. Capture the exact command output — signature failures are the one class of finding that must never be reproduced "later from memory".
  3. Check whether the release run itself is intact (gh run view <run-id>); a re-run with different inputs is a much more likely explanation than a compromise, but it must be confirmed, not assumed.
  4. Escalate per docs/runbooks/ before overriding anything.

Self-hosted Tesla intelligence · MIT licensed · Privacy policy