Gitfence

Verify a Gitfence release

Verify a self-hosted Gitfence release with cosign: key fingerprint, signed image digest, provenance, SBOM, release files and the running executable.

Short answer: Every self-hosted Gitfence release is signed. You can verify the container image and release files with cosign, inspect signed provenance, SBOM and vulnerability attestations, pin the signed image digest, and check the executable that is running.

You run Gitfence in your own infrastructure without its source code. These checks let you verify what you install without trusting us on our word.

What each release carries

  • Cosign signatures (Sigstore): on the container image (by digest), on each executable, and on SHA256SUMS, which lists every file of the release.
  • Build provenance: an in-toto attestation in the SLSA v1 format. It records the source commit, the builder, the build command, and the inputs and outputs with their digests.
  • An SBOM in CycloneDX format: the npm packages compiled into the executable, the Bun runtime, and the image's packages (Wolfi apk packages and the Go modules in the MongoDB tools).
  • A vulnerability scan of that SBOM (Grype), with the date of the database it used.
  • A signed VEX document (OpenVEX): for each known vulnerability in the image that Gitfence can't reach, the reason. Your scanner applies it.
  • A record in the public Rekor transparency log for every signature: an append-only public log, so a signature can't be quietly made or replaced later.

The commands below need cosign 2.4 or later (3.x tested), plus jq and sha256sum. Replace 1.0.0 with your version.

1. Get the public key and check it

Releases signed by HootCodes use one cosign key. Its public half is cosign.pub:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3opCq7y9grBiqHBHv6hrBVzLbbxh
EynB/GSHeAK8ScjVB1gQGUoQZohEUgrJWFuCoUKYLdfiUESvJd0xL+ek/A==
-----END PUBLIC KEY-----

It is also published at https://gitfence.dev/cosign.pub and comes with every release. Before you trust it, compare its fingerprint with the one in your contract or onboarding email, which reaches you separately from the download:

openssl pkey -pubin -in cosign.pub -outform DER | sha256sum
# 9bd625e20517441a39ecb41c83736f5c8a97ef54e3b0fef4b73c82c614f4e608

Releases built by our GitHub Actions workflow are also signed keyless: by the workflow's own identity instead of a key. To check those, use this identity and issuer instead of --key cosign.pub:

--certificate-identity-regexp '^https://github.com/hoot-codes/gitfence/\.github/workflows/release\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

2. Verify the image

Log in to the registry with the read-only token we gave you (docker login ghcr.io); cosign uses the same credentials.

cosign verify --key cosign.pub ghcr.io/hoot-codes/gitfence-selfhost:1.0.0

This checks that the image the tag points to is signed by our key and that the signature is in Rekor. It prints the signed digest (docker-manifest-digest). Then pin that digest so a tag that moves later can't change what you run. In your compose .env:

GITFENCE_VERSION=1.0.0@sha256:<the digest cosign printed>

Docker then pulls exactly that image, and Gitfence shows the pinned reference on its License page and at /health/build.

Provenance, SBOM and vulnerability scan (attached to the image)

# Provenance: source commit, builder, command, inputs
cosign verify-attestation --key cosign.pub --type slsaprovenance1 ghcr.io/hoot-codes/gitfence-selfhost:1.0.0 \
  | jq -r .payload | base64 -d | jq '{subject, source: .predicate.buildDefinition.externalParameters.source,
      builder: .predicate.runDetails.builder.id, command: .predicate.buildDefinition.externalParameters.command}'

# SBOM (CycloneDX)
cosign verify-attestation --key cosign.pub --type cyclonedx ghcr.io/hoot-codes/gitfence-selfhost:1.0.0 \
  | jq -r .payload | base64 -d | jq .predicate > gitfence-sbom.cdx.json

# Vulnerability scan: counts, scanner and database date
cosign verify-attestation --key cosign.pub --type vuln ghcr.io/hoot-codes/gitfence-selfhost:1.0.0 \
  | jq -r .payload | base64 -d | jq '.predicate.scanner | {version, db, summary: .result.summary}'

Each command verifies the attestation's signature before printing anything. You can feed the SBOM into your own scanner: grype sbom:gitfence-sbom.cdx.json, trivy sbom gitfence-sbom.cdx.json, or Dependency-Track.

Scan it yourself, with our VEX applied

cosign verify-attestation --key cosign.pub --type openvex ghcr.io/hoot-codes/gitfence-selfhost:1.0.0 \
  | jq -r .payload | base64 -d | jq .predicate > gitfence.openvex.json
grype ghcr.io/hoot-codes/gitfence-selfhost:1.0.0 --vex gitfence.openvex.json

Grype applies the VEX to image scans (registry, docker:, or the air-gapped tarball as docker-archive:), not to an SBOM file scanned alone. Each statement names the image by digest and the affected package versions, so it applies to that release only. Findings it doesn't cover stay in the result.

Since 1.0.0-rc3 the image is built on Wolfi, a minimal distribution rebuilt as fixes land, and its scan shows no known vulnerabilities, so there is nothing for a VEX to explain. A release only carries a VEX when a scan finds something we have analysed and can honestly show Gitfence doesn't reach; 1.0.0-rc2's covered fifteen libcurl CVEs in the Debian image it used.

3. Verify the release files (also air-gapped)

The release files are SHA256SUMS, its signature SHA256SUMS.sigstore.json, and, depending on how you install, the executables, the image tarball and the compose bundle. With all of them in one directory:

cosign verify-blob --key cosign.pub --bundle SHA256SUMS.sigstore.json SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing

The first command shows that SHA256SUMS is ours. The second shows that every file you have is the one it lists. Each executable also has its own signature:

cosign verify-blob --key cosign.pub --bundle gitfence-linux-x64.sigstore.json gitfence-linux-x64

and the provenance is signed for every file it names:

cosign verify-blob-attestation --key cosign.pub --type slsaprovenance1 \
  --bundle provenance-1.0.0.intoto.sigstore.json gitfence-selfhost-1.0.0-amd64.tar.zst

Without internet access, cosign can't reach Rekor. Each bundle carries its log entry and its inclusion proof, so verify-blob still checks them offline against Sigstore's trusted root. If your cosign has no trusted root cached, copy one from a connected machine (cosign trusted-root create, or ~/.sigstore/root/).

The air-gapped image tarball. After zstd -dc gitfence-selfhost-1.0.0-amd64.tar.zst | docker load, compare the image id with the signed provenance:

docker image inspect gitfence/selfhost:1.0.0 --format '{{.Id}}'
jq -r '.predicate.runDetails.byproducts[] | select(.name | contains("image config")) | .digest.sha256' provenance-1.0.0.intoto.json

4. Verify what is running

Gitfence reports what it is:

curl -s http://localhost:8080/health/build
# {"version":"1.0.0","commit":"…","binary":{"name":"gitfence-linux-x64","sha256":"…"},"image":"ghcr.io/…@sha256:…"}

The Billing page (the self-hosted license) shows the same: the version, the commit, the executable's SHA-256 and the configured image.

Gitfence can also check itself against the signed checksums of its release:

docker compose exec gitfence gitfence verify

It hashes the running executable, fetches SHA256SUMS and its signature from https://gitfence.dev/releases/<version>/, checks the signature with the cosign key built into the executable, and finds the executable's line. It exits 0 when it matches. Offline, give it the files instead. With --key, it uses the copy of the key you checked in step 1:

docker compose exec gitfence gitfence verify --sums /path/SHA256SUMS --bundle /path/SHA256SUMS.sigstore.json --key /path/cosign.pub

A program can't fully vouch for itself: an altered executable could lie about its own hash. For an independent check, hash the executable inside the image from outside and compare it with SHA256SUMS:

id=$(docker create ghcr.io/hoot-codes/gitfence-selfhost:1.0.0) && docker cp "$id":/usr/local/bin/gitfence ./gitfence-from-image \
  && docker rm "$id" >/dev/null && sha256sum gitfence-from-image && grep gitfence-linux-x64 SHA256SUMS

5. The transparency log

Every signature of a published release is in Rekor. cosign verify and verify-blob check that by default. To see the entries yourself, find their log indexes in trust.json (.cosign.rekor.logIndexes) or in a bundle (.verificationMaterial.tlogEntries[].logIndex), and open https://search.sigstore.dev/?logIndex=<index>. Rekor only holds hashes and signatures, never our files or yours.

A release we sent before publishing it (a release candidate) can carry local signatures that aren't in Rekor yet. Its trust.json says "rekor": {"uploaded": false}, and checking it needs --insecure-ignore-tlog until we publish it with logged signatures.

6. Reproducible builds

The Gitfence executable is built with bun build --compile. That build is deterministic: the same commit, the same bun.lock and the same Bun version give byte-identical executables. We checked this by rebuilding 1.0.0-rc1 from its commit in a separate checkout, hours later. Both architectures matched the shipped files byte for byte. We also built one commit on GitHub's Actions runners and on our release workstation: both architectures came out identical. The provenance records everything that goes in: the commit, the lockfile's hash, the Bun version, and the hashes of the Bun runtime and the native module compiled in.

Obfuscation keeps this chain. Since 1.0.0-rc3 our modules are obfuscated as they are bundled (to deter reverse engineering; not a security boundary; use is governed by the license). The obfuscator runs with a fixed seed and pinned options, so the chain stays deterministic end to end: source tree → obfuscated bundle → executable hash. Two builds of one commit give the same bytes, and the provenance records the obfuscator's version and a digest of its options. Support keeps the symbol maps that turn a stack trace back into file and line. They are signed with the release and never leave us.

What this means for you:

  • You can't rebuild it yourself without our source. On request, an auditor or an escrow agent under NDA can rebuild from the commit in the provenance and confirm the executable's hash.
  • The image is not reproducible. Its base is pinned by digest, but it installs the distribution's current packages at build time (Wolfi's, which carry the latest fixes), so two builds differ. What you can verify instead: that the image digest is signed (step 2), and that the executable inside it is the signed, reproducible one (the docker cp check in step 4). Everything else in the image is listed in the SBOM.

7. What each check shows

Check Shows Doesn't show
cosign verify (key) HootCodes signed this exact image, and the signature is in a public log that our build machine was clean
Keyless signature (GitHub Actions) it was built and signed by our release workflow, at that tag, on GitHub's runners anything about the source beyond the commit
Provenance from which commit, how, and from what inputs it was built that the source is bug-free
SBOM and scan what's inside, and the known vulnerabilities on the scan date vulnerabilities found later: re-scan the SBOM yourself
sha256sum -c your files are the signed ones

SLSA level. Releases built by our GitHub Actions workflow reach SLSA Build Level 2: a hosted build platform generates and signs the provenance. Releases built on our release workstation carry the same signed provenance but count as Build L1, because the builder is not a hosted platform. The provenance's builder.id says which.

Vulnerabilities. The scan is published as it is, findings included, with every release. trust.json sums it up (vulnScan.counts, and ok is true when there are no critical or high findings). Most findings in a container image come from the base distribution. The SBOM marks which components are compiled into the executable (gitfence:bundled-into) and which come from the image (gitfence:found-in).