Skip to main content

Supply chain and release verification

This page describes how HaltSQL releases are built and published, and how to verify an artifact before you run it. It is for security reviewers and platform teams who need to check signatures, provenance and SBOMs, or answer supply-chain questions about HaltSQL. Everything here is taken from the workflows in .github/workflows/, the Dockerfile and .github/dependabot.yml.

What a release contains​

A release is triggered by pushing a vX.Y.Z tag (or by running the release workflow by hand for an existing tag). Release-candidate tags (vX.Y.Z-rc.N) and manual runs with dry_run are rehearsals: they run every build, signing and provenance step but publish only a draft release and packages under ghcr.io/haltsql/dryrun/; see releasing.md. .github/workflows/release.yml produces:

ArtifactWhereBuilt by
haltsql-<tag>-linux-amd64.tar.gz, haltsql-<tag>-linux-arm64.tar.gzGitHub releaseThe dist stage of the Dockerfile: static musl binaries haltsql, haltsql-server, haltsql-proxy, plus licences
haltsql-<tag>-darwin-arm64.tar.gzGitHub releaseA macos-15 runner with ocaml/setup-ocaml; links only system libraries
haltsql-<tag>.cdx.jsonGitHub releaseCycloneDX SBOM (see SBOMs)
haltsql-<version>.tgzGitHub releaseThe Helm chart archive (helm package deploy/helm/haltsql)
haltsql-<tag>.oci.txtGitHub releaseThe digests of the three images and the chart as pushed to GHCR
SHA256SUMSGitHub releaseSHA-256 of every tarball, the chart archive, the SBOM and haltsql-<tag>.oci.txt
*.sigstore.jsonGitHub releaseKeyless cosign signature bundle for every file above and SHA256SUMS
ghcr.io/haltsql/haltsql:<version>GHCRThe runtime stage of the Dockerfile, linux/amd64 and linux/arm64
ghcr.io/haltsql/haltsql-mcp:<version>GHCRclients/mcp/Dockerfile, on top of the haltsql image by digest
ghcr.io/haltsql/haltsql-console:<version>GHCRThe console stage of the Dockerfile, linux/amd64 and linux/arm64
oci://ghcr.io/haltsql/charts/haltsql, version <version>GHCRThe same chart archive, pushed with helm push

.github/workflows/native-packages.yml publishes, on v* tags:

ArtifactWhereNotes
haltsql wheels and sdist, haltsql-mcp wheel and sdistPyPITrusted publishing (OIDC, no long-lived PyPI token), through the pypi and pypi-mcp environments
@haltsql/client and @haltsql/native-<platform>npmPublished with npm publish --provenance, through the npm environment, by npm trusted publishing (OIDC); the first publish only uses a short-lived NPM_TOKEN

Not published by these workflows: the Kubernetes operator image (you build it from deploy/operator, see kubernetes-operator.md), the Java SDK and the Go SDK (Go modules are fetched from the repository).

Build integrity​

ControlWhat existsSource
Pinned GitHub ActionsEvery third-party action in every workflow is pinned to a full commit SHA, with the version in a comment.github/workflows/*.yml
Pinned base imagesBuilder (ocaml/opam:alpine-3.22-ocaml-5.3) and runtime (gcr.io/distroless/static-debian12:nonroot) pinned by digest; the Dockerfile syntax image tooDockerfile
Pinned OCaml dependenciesThe Linux release build resolves opam packages against a fixed opam-repository commit (OPAM_REPOSITORY_COMMIT), so tarballs and image ship the same dependency versions. The resolved list is written to /usr/share/doc/haltsql/opam-packages.txt in the image.Dockerfile
Vendored parserlibpg_query is vendored in vendor/libpg_query and compiled from sourceDockerfile, vendor/
Static linking checkThe build fails unless every binary is statically linked, runs, and runs on an empty (scratch) filesystemDockerfile, release.yml
Smoke test before pushEach architecture's image is built on a native runner (amd64 and arm64, no emulation), run against PostgreSQL 17 by scripts/docker-smoke.sh (including the proxy), and only then pushed by digest and merged into the multi-arch indexrelease.yml
Least-privilege workflow tokensWorkflows default to permissions: contents: read; jobs that publish add only packages: write, id-token: write, attestations: write or contents: write as needed.github/workflows/*.yml
No persisted checkout credentialsactions/checkout runs with persist-credentials: false.github/workflows/*.yml
Workflow lintingactionlint, from a digest-pinned image, on every PRsecurity.yml

Known gaps:

  • The macOS release tarball, the macOS wheel and npm package, and the Linux libhaltsql.so in the wheels and npm packages are built with opam install --deps-only against the current opam-repository, not the pinned commit. Their OCaml dependency versions can differ from the Linux tarballs and the image.
  • The manylinux_2_28 build container used for libhaltsql.so is referenced by tag, not digest.
  • Builds are not documented or tested as bit-for-bit reproducible.
  • The macOS binaries are not signed with an Apple Developer ID or notarized. (The libhaltsql dylib in the macOS wheel has an ad-hoc signature only.)

Signatures and provenance​

ArtifactSignatureProvenance
Release tarballsKeyless cosign (cosign sign-blob --bundle)GitHub build-provenance attestation (actions/attest-build-provenance)
Helm chart archiveKeyless cosign (cosign sign-blob --bundle)GitHub build-provenance attestation
SHA256SUMS, SBOM, haltsql-<tag>.oci.txtKeyless cosignNone
Helm chart in GHCRKeyless cosign on the chart's OCI digest, verified in the same jobGitHub build-provenance attestation pushed to the registry
haltsql, haltsql-mcp and haltsql-console imagesKeyless cosign on the image digest, verified in the same jobBuildKit SLSA provenance (provenance: mode=max) attached to the image index, plus a GitHub build-provenance attestation pushed to the registry
PyPI filesTrusted publishing through pypa/gh-action-pypi-publishSee the PyPI file page
npm packagesnpm registry signaturesnpm provenance (--provenance)

Keyless signatures are issued by Sigstore to the identity of the GitHub Actions workflow that ran, so there is no long-lived signing key to protect or leak. The certificate identity is the workflow file and ref, for example https://github.com/HaltSQL/HaltSQL/.github/workflows/release.yml@refs/tags/v0.0.1, and the OIDC issuer is https://token.actions.githubusercontent.com.

Note: a release rebuilt by hand (workflow_dispatch) runs from the branch the workflow was dispatched on, so its certificate identity ends in @refs/heads/<branch> instead of @refs/tags/<tag>. The commands below accept only tag identities. If one fails for this reason, check with the maintainers before relaxing the identity.

SBOMs​

SBOMFormatProduced byCovers
haltsql-<tag>.cdx.json (release asset)CycloneDX JSONanchore/sbom-action (Syft), run on the source tree (path: .)The dependency manifests Syft finds in the repository
Image SBOM attestationSPDX (BuildKit default)docker/build-push-action with sbom: trueThe contents of the built image

Warning: the release binaries are static OCaml executables. Neither SBOM is guaranteed to list every OCaml library linked into them, and the source-tree SBOM also includes manifests from parts of the repository that are not in the binaries (SDKs, the docs site, the lab). For the exact OCaml packages in the image and Linux tarballs, use /usr/share/doc/haltsql/opam-packages.txt from the image. That file is not included in the tarballs.

There are no VEX documents.

Scanning and dependency updates​

ControlWhat runsWhen
Container image scanTrivy (digest-pinned) on the built image, HIGH and CRITICAL, unfixed ignored; fails the job on findings; SARIF uploaded to GitHub code scanningWeekly (Mondays) and on demand; not on pull requests
Configuration and secret scanTrivy fs --scanners misconfig,secret on the repository, MEDIUM and above, SARIF uploadedWeekly and on demand
Dependency reviewactions/dependency-review-action, fails on high severity; denies AGPL and SSPL licencesEvery pull request (public repository only)
Static analysis of OCaml codeNone. CodeQL does not support OCaml.
DependabotWeekly, with a 7-day cooldown on new releases: GitHub Actions, the root Dockerfile base images, npm (clients/typescript, website), pip (clients/python, clients/mcp, lab), Go modules (clients/go/...), Maven (clients/java).github/dependabot.yml

Dependabot does not support opam. OCaml dependencies move only when the OPAM_REPOSITORY_COMMIT in the Dockerfile is bumped by hand. Dependabot is not configured for deploy/operator (Go) or for clients/mcp/Dockerfile and deploy/operator/Dockerfile base images.

Vulnerability reports go through GitHub Security Advisories (SECURITY.md). Only the latest release receives security fixes while HaltSQL is pre-1.0.

Verifying a release​

You need cosign v2.4 or later (v3 works too), the GitHub CLI 2.49 or later, and jq. Replace v0.0.1 with the release you are checking.

scripts/verify-release.sh v0.0.1 runs every command below (and checks that every asset is present, listed in SHA256SUMS and signed) against a downloaded release; the release workflow runs it on every release before anyone else can. The steps by hand:

Release tarballs​

TAG=v0.0.1
gh release download "$TAG" --repo HaltSQL/HaltSQL --dir "haltsql-$TAG"
cd "haltsql-$TAG"

ID='^https://github.com/HaltSQL/HaltSQL/\.github/workflows/release\.yml@refs/tags/v'
ISSUER=https://token.actions.githubusercontent.com

# 1. The checksum file was signed by the release workflow
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity-regexp "$ID" --certificate-oidc-issuer "$ISSUER" \
SHA256SUMS

# 2. Every downloaded file matches it
sha256sum -c SHA256SUMS --ignore-missing # macOS: shasum -a 256 -c SHA256SUMS

# 3. A tarball's own signature
cosign verify-blob --bundle "haltsql-$TAG-linux-amd64.tar.gz.sigstore.json" \
--certificate-identity-regexp "$ID" --certificate-oidc-issuer "$ISSUER" \
"haltsql-$TAG-linux-amd64.tar.gz"

# 4. Build provenance: built by this repository's workflow from this commit
gh attestation verify "haltsql-$TAG-linux-amd64.tar.gz" --repo HaltSQL/HaltSQL

Container images​

IMAGE=ghcr.io/haltsql/haltsql:0.0.1
ID='^https://github.com/HaltSQL/HaltSQL/\.github/workflows/release\.yml@refs/tags/v'

# Signature
cosign verify "$IMAGE" \
--certificate-identity-regexp "$ID" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

# GitHub build-provenance attestation
gh attestation verify "oci://$IMAGE" --repo HaltSQL/HaltSQL

# BuildKit SBOM and SLSA provenance attached to the image index
docker buildx imagetools inspect "$IMAGE" --format '{{ json .SBOM }}'
docker buildx imagetools inspect "$IMAGE" --format '{{ json .Provenance }}'

Deploy by digest (ghcr.io/haltsql/haltsql@sha256:...), not by tag; the release notes print the digest, and haltsql-<tag>.oci.txt (covered by the signed SHA256SUMS) lists the digest of each image and the chart. The same commands work for ghcr.io/haltsql/haltsql-mcp. To enforce signatures at admission time, use Sigstore policy-controller or Kyverno with the identity and issuer above.

Helm chart​

CHART=ghcr.io/haltsql/charts/haltsql
DIGEST=$(awk '$1 == "helm-chart" {sub(/.*@/, "", $2); print $2}' haltsql-$TAG.oci.txt)

cosign verify "$CHART@$DIGEST" \
--certificate-identity-regexp "$ID" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify "oci://$CHART@$DIGEST" --repo HaltSQL/HaltSQL

helm install haltsql "oci://$CHART" --version "${TAG#v}" # or pull the verified release asset

The chart's content layer in the registry is byte-for-byte the haltsql-<version>.tgz release asset, so the asset's signature and attestation cover the registry copy too.

npm packages​

Note: the Python and npm packages are not published to PyPI or npm yet. These checks apply once the release workflow publishes them; until then, install from a verified source tarball or the repository.

npm install @haltsql/client
npm audit signatures # checks registry signatures and provenance attestations

Python packages​

The wheels and sdist are uploaded by trusted publishing from .github/workflows/native-packages.yml. Check the file's provenance on its PyPI page, and pin hashes in production (pip install --require-hashes -r requirements.txt).

Policy bundles are a separate chain​

The signatures above cover HaltSQL itself. Your policies have their own supply chain: signed with your Ed25519 key in your CI, and verified by every server before any policy is read. See policy-bundles.md and policy-ci.md.