1. What is protected
Tool versions (plan section 5): cosign 3.1.4, syft 1.57.1, Trivy 0.76.2, gitleaks 8.30.1,
govulncheck 1.9.0. Workflows install them with
scripts/supply-chain/install-tools.sh, which checks
each download against the release’s own checksum file. Every GitHub Action is pinned by commit SHA
(resolved with git ls-remote, tag in a comment).
2. Image release (release-images.yml)
For each image, in .github/actions/release-image/action.yml:
- Build with BuildKit and push to
ghcr.io/<owner>/<repo>/<name>:<tag>. The postgres-ha tag is<postgresql version>-<tag>, because the core role compares the version. scripts/supply-chain/scan-image.sh <repo>@<digest>: an SPDX SBOM with syft, the full Trivy report as JSON, then the gate: Trivy with--severity CRITICAL,HIGH --ignore-unfixed --exit-code 1. Unfixed findings are recorded but do not fail the release, because only an upstream fix or a mitigation can address them (see the runbook).- Only an image that passed the gate is signed:
cosign sign --yes <repo>@<digest>. Keyless: a short-lived Fulcio certificate issued to this workflow’s GitHub OIDC identity, recorded in the Rekor transparency log. No signing key exists anywhere. actions/attestv4.2.2 writes a build provenance attestation and an SBOM attestation for the digest and pushes both to the registry.
release-images.json (schema 1: release tag, and per image the repository,
tag and digest; scripts/supply-chain/release-manifest.py refuses a set of names other than the
expected eleven), verifies every digest again with the deploy’s identity, attests the file, and
attaches it to the GitHub release.
3. Deploy-time verification (every playbook that pulls images)
site.yml (first converge and node replacement), deploy.yml (rolling deploy), rotate-all.yml
and restore.yml all start with playbooks/verify-release.yml, whose first play runs
infra/ansible/playbooks/tasks/verify-images.yml on the controller (finding O-8; before
2026-11-13 only deploy.yml did, so the documented first converge pulled by tag). The local
converge is the one exception, refused for any other environment: its images come unsigned from
its throwaway registry, and the play says so instead of verifying.
- no manifest is given, or it names another release, another registry, or not every image in
platform_images; - an upstream image in
vars/pins.ymlis not pinned by digest; cosign verifyfails for any digest with identityhttps://github.com/justmert/veristar/.github/workflows/release-images.yml@refs/tags/<release>and issuerhttps://token.actions.githubusercontent.com(vars/signing.yml). This covers an unsigned digest and a signature from any other workflow, branch, tag or repository;- the release tag in the registry now points to a digest other than the manifest’s.
platform_images on every node to the verified repo:tag@digest references.
Host facts outrank play variables, so the roles (including the imported bootstrap play) pull exactly
the verified digests. The roles themselves are unchanged.
Local proof (test-only key). Keyless signing needs GitHub’s OIDC token, which exists only inside
GitHub Actions. tests/supplychain (make supplychain-it) proves the refusal logic with a real
registry and real cosign signatures made with an ephemeral key pair created for one test run
(sa_signature_mode: test-ephemeral-key). The task file refuses that mode for any inventory outside
tests/, and a test case proves the refusal. This is a test of the verification logic, not the
production signing design. Result on 2026-11-13: 10 of 10 cases pass (signed release passes and pins
digests; unsigned digest, foreign key, other build under the release tag, moved tag, other release,
other registry, incomplete manifest, missing manifest, test mode outside tests: all refused).
Status. Built and proven locally with the test-only key. Keyless signing and cosign verify of a
real release have not run: GitHub Actions on the account ends every workflow in startup_failure.
Artifact attestations for a private repository also need GitHub Enterprise Cloud, so attestations
need the repository to be public (owner’s explicit confirmation, plan phase 18). The probe
node’s own images (the prober and the custom Caddy, from the observability role) are verified the
same way: the manifest must name the prober (it is in platform_images), the pin play sets
platform_images on the probe too, and the observability role names its prober and caddy
through platform_images. The role also refuses a probe node whose prober or caddy is not
pinned by digest outside the local converge, so a run that skipped the check stops. The probe
converge has not completed on the local converge yet (make ops-local). The probe bundle of
docs/deploy/probe.md uses the same check: playbooks/probe-bundle.yml imports
verify-release.yml and refuses a prober not pinned by digest.
4. Tracker release (release-tracker.yml)
A tag tracker-v<x.y.z> must equal packages/tracker/package.json and the newest entry of
releases/manifest.json. The prove job runs verify-releases (committed files unchanged,
consistent manifest, byte-identical rebuild of sdk.js on the runner, size limit) and the tracker
tests, including test/pack.test.ts: the npm package packs to the same tarball twice, installs into
an empty consumer, works with no __SDK_VERSION__ define (the published ESM carries the version),
type-checks, and carries sdk.js byte for byte. The publish job attests sdk.js and the tarball,
then runs npm publish with trusted publishing: npm exchanges the job’s OIDC token for a
short-lived publish credential and attaches provenance itself. It then creates the GitHub release
with sdk.js, the manifest and the tarball.
Status. Built; the package build and its tests pass locally (5 of 5 in pack.test.ts, run with
a test-only scope @sa-local-test). Blocked: input I9 (the npm scope with this repository and
workflow registered as trusted publisher; set as repository variable NPM_PACKAGE), GitHub
Actions on the account, and a public repository (npm provenance and trusted publishing require it).
The control plane still refuses TRACKER_NPM_PACKAGE until the package is published.
5. Detection (security.yml, hourly)
scripts/supply-chain/advisories.py: every published repository advisory ofumami-software/umaminot listed indocs/security/advisories-reviewed.json, any newer Umami release, GitHub advisory database matches for Next.js, React and React DOM at the exact versions in the pinned Umami’spnpm-lock.yaml, for nats-server, Caddy, etcd and the collector modules at our pins, and a newer PostgreSQL minor of the pinned major. It never reads “fixed in” wording, because Umami has published advisories without a fixed version. A source that cannot be read fails the job; it is never treated as clean.- Trivy on the digests a deploy uses: the platform images of the newest
release-images.jsonand every upstream digest pin. govulncheckon both Go modules,pnpm auditon the workspace.- Every finding opens one issue labelled
security-finding(an open issue for the same id is not repeated) and the run posts one alert toALERT_WEBHOOK_URL(input I8). Without I8 the run ends red after filing the issues, so GitHub’s own failure notice still reaches the maintainers.
.github/dependabot.yml) opens update pull requests for Go, npm, Dockerfiles,
compose files, GitHub Actions, OpenTofu providers, the Python and Rust examples and the Ansible
controller requirements. Umami is excluded: its digest moves only through make umami-upgrade.
The digests in infra/ansible/vars/pins.yml are not a format Dependabot reads; the hourly Trivy
run covers them.
6. Pull request gates (ci.yml)
gitleaks (whole history, reviewed fingerprints in .gitleaksignore), licences
(scripts/supply-chain/licences.py, see section 7), isolation (make isolation) and
supplychain (make supplychain-it), next to the existing build, lint, test and tracker checks.
branch-protection.md lists the required checks.
7. Licences
Policy. Everything this project ships, and every production dependency of it, carries a licence on the OSI approved list. There are no exceptions for shipped code. Dependencies that are never shipped (development tools, test fixtures, example apps and operator tooling) are listed in a separate section of the report. Any of them under a licence that is not OSI approved must be on an explicit allowlist, reviewed for one exact version and licence, with a written reason. Nothing is skipped silently: a package that cannot be placed in either section, or whose licence cannot be found, fails the gate.scripts/supply-chain/licence-gate.sh (the licences job in ci.yml, and local runs) feeds
scripts/supply-chain/licences.py, which reads docs/security/licence-policy.json:
Three checks keep the shipped section honest instead of trusting
package.json:
- The console bundle is built with its own Vite configuration and every npm package whose code lands
in it is listed (
apps/console/scripts/bundle-packages.mjs). Each must be in the shipped section, so a package imported at runtime but declared as a devDependency fails the gate. CSS is compiled by the Tailwind plugin and is not part of that module graph, so the policy names whatsrc/index.cssimports:tailwindcss(a devDependency) counts as shipped, and ofshadcnonly the fileshadcn/tailwind.cssis used, soshadcnis shipped but the dependencies of its CLI are not. - The tracker builds (
sdk.jsinbuild.mjs, the npm package inscripts/pack.mjs) fail when esbuild reports any input outsidesrc/andreleases/, and the packedpackage.jsondeclares no dependencies. The tracker therefore ships no third-party code, including no Wallets Kit code. - Go: only modules that are linked into a shipped binary count as shipped. Test-only modules such as testcontainers are listed as not shipped.
deploy/images/caddy.Dockerfile with two plugins) and the operating system packages of the base
images (distroless, Alpine in the Caddy image, Debian in postgres-ha) are not part of these
graphs. The image SBOMs from scan-image.sh list them; checking their licences is a separate step
that does not exist yet.
Why the Wallets Kit packages are allowlisted. @creit.tech/stellar-wallets-kit 2.7.1 and 2.8.0
are installed only as devDependencies of packages/tracker (swk-2.7.1, swk-2.8.0, so the phase
14 adapter tests run the real kit) and as a dependency of the example app
examples/wallets-kit-spa, a sample for tenants that is also a test target and is never deployed.
The lockfile graph shows no other path to them. Checked on 2026-11-13: esbuild’s input list for
sdk.js and for the npm entry points contains only src/ and releases/ files; the packed npm
package (@sa-local-test/tracker 1.1.0) has no dependency fields, no bare imports and no kit
code; the console bundle contains none of the kit’s packages and no walletconnect, reown,
creit.tech, xbull or trezor string; the SBOMs of all 12 platform images list none of them. The
kit pulls in 26 lockfile entries the gate reports as non-OSI (25 distinct versions): 19 Reown and
WalletConnect versions and @trezor/crypto-utils under “SEE LICENSE IN LICENSE.md” (not an OSI
licence), and five with no licence in their registry metadata (@creit.tech/xbull-wallet-connect,
@hot-wallet/sdk, @near-wallet-selector/core, eyes, text-encoding-utf-8). The project
does not bundle or redistribute any of them, so they are allowlisted as test and example
dependencies. Tenants who add the kit to their own sites take it from npm under its own terms.
The other allowlisted packages are data, not code: caniuse-lite (CC-BY-4.0, browser support data
for build tools; it reaches the console importer only through the shadcn CLI and is not bundled),
mdn-data (CC0-1.0, CSS reference data for test and example tooling) and webpki-root-certs
(CDLA-Permissive-2.0, root certificates in the Rust example app). Before 2026-11-13 these three were
exceptions that applied wherever the package appeared; they are now accepted only while not
shipped.
Changing the allowlist. Add or update an entry in not_shipped_allowlist with the exact
version and the licence string the report printed, a reason that says why the package is not
shipped and what it is, and the review date. The reviewer checks the report’s “pulled in by” line.
A version bump of an allowlisted package fails the gate until the entry is reviewed again.
8. Results recorded on 2026-11-13
- Trivy and syft on all 12 platform images built from this tree and the 11 upstream images. All 12
platform images pass the gate after three fixes:
github.com/klauspost/compress1.20.1 to 1.20.4 (GO-2026-6865, called from the replayer), Caddy built withgolang.org/x/netv0.62.0 (CVE-2026-78661 and others), andpostgres-hawithout the unusedgosu(Go 1.24.6 stdlib, one critical and ten high) and with the Debianzlib1gsecurity update. - Upstream images with fixable high findings that only their maintainers can fix: Umami 3.5.0
(
undici6.28.0), HAProxy 3.4.7 (zlib1g), etcd 3.6.16, Prometheus 3.15.1, Alertmanager 0.34.2, Grafana 13.2.6, Gatus 5.37.0, node_exporter 1.12.1, postgres_exporter 0.20.1, prometheus-nats-exporter 0.20.3. Section 8.1 records, per finding, the pin bump or the mitigation and its reasoning. - govulncheck: no called vulnerability in either module after the fixes (the example app moved to
golang.org/x/netv0.62.0). One uncalled module finding remains (golang.org/x/crypto/openpgp, GO-2026-5932, fixed only in a pseudo-version; the package is not imported). - A throwaway build of
postgres-hawithgosuleft in fails the gate: 1 critical, 25 high, exit 1.
8.1 Upstream image findings: bump or mitigation (2026-11-13)
Every fixable critical or high finding of the upstream images, rechecked on 2026-11-13 with Trivy 0.76.2 (--scanners vuln, registry images by digest, one at a time). For each image the
registry was checked for a release that fixes it (plan section 5 pin policy: a younger minor line
is taken when it carries a security fix). Where no release fixes a Go finding, the image’s binary
was checked with govulncheck -mode=binary (v1.9.0, database of 2026-11-11), which reports
whether the vulnerable symbols are compiled into the binary at all. No critical finding exists in
any upstream image. Nothing here was run against production, which does not exist yet.
Bumped (each rescanned after the change):
Pins moved together in
infra/ansible/vars/pins.yml, the observability role defaults,
deploy/images/upstream/ (tests/pins checks they agree) and tests/observability/compose.yml.
Mitigated (no fixed release exists on 2026-11-13):
The hourly
security.yml workflow keeps filing these findings until a fixed release exists. A
fixable high finding that is not in this table has no recorded decision and is handled under the
24-hour rule (docs/runbooks/security-patch.md).