Skip to main content
Jeppe Lillevang Salling

Colophon · running v1.2.1

How this site is built

The stack, the release flow, and enough to check my work: the signing key, a verify command, and the SBOM and vulnerability report for every release.

Stack

Astro 7 with static output, Svelte 5 for the few interactive islands, Tailwind 4 for styling. The build is copied into an nginx 1.31 Alpine container that runs as an unprivileged user behind a Cilium gateway on lillecloud, my Talos cluster on Hetzner (more on the now page). ArgoCD deploys whatever image tag the infrastructure repository names. Renovate opens the dependency and base-image pull requests. There is no hosted CI.

Release flow

Every release is cut from my own machine with a Taskfile. The cosign key and its password live in 1Password and are read into the environment for the one command that needs them. They are never written to disk and there is no CI runner to leak them from.

  1. Preflight: refuse to release from any branch but main, or to republish a version that already exists in the registry.
  2. Build the image with Podman and run the health and smoke checks against it (headers, compression, caching, vhost allowlist).
  3. Push to ghcr.io.
  4. Resolve the digest from the registry and sign that digest with cosign. The tag is never what gets signed.
  5. Verify the signature offline with the public key, exactly as the cluster policy does.
  6. Trivy writes a CycloneDX SBOM and a vulnerability report for the image.
  7. Commit the version, the digest, the SBOM and the report to the repository.

Deployment is a separate step: Renovate in the infrastructure repository notices the new tag and opens the pull request that bumps it.

Why a key, not keyless

The signature is a plain key pair rather than a keyless identity tied to a GitHub login. If I leave GitHub, or GitHub leaves me, the key still verifies every image I ever signed. The price is transparency: the signatures are not uploaded to Rekor, so a verifier has to say so explicitly with the flag in the command below.

Verify it yourself

The current public key is served at /cosign.pub and committed in the repository as cosign.pub. Releases from July 2026 onward are signed with it; the three releases before that which carry a signature were signed with the 2025 key. The table below says which is which.

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcinYmcKT7QBe48DesANXk+P/uGYY
OIGoPhTYeYOs+4N1RWXQOOKfoTWk6zfK/R2IzGvnJeGnNNTbQlMwFYgp4A==
-----END PUBLIC KEY-----

This verifies the image behind the newest signed release, v1.2.0:

curl -sO https://lillevang.dev/cosign.pub
cosign verify --key cosign.pub --insecure-ignore-tlog=true \
  ghcr.io/lillevang/website@sha256:43ffa47f2679fe51e016557e6ec22e46b20c39ea675286b50d729b1c982552b6

The package on ghcr.io is public, so this needs no login. The same command, with the key read from 1Password, is what the release pipeline runs before the Trivy step.

Releases

VersionDateDigestSigned withFindingsArtifacts
1.2.02026-10-13sha256:43ffa47f2679fe51…current keynoneSBOMreport
1.1.222026-10-13sha256:d37cb1d574c33e3f…current keynoneSBOMreport
1.1.212026-09-14sha256:d000c0ba1e1d13dc…current keynoneSBOMreport
1.1.202026-09-13sha256:a433d5edd60c0cb8…current key4 high, 14 medium, 32 lowSBOMreport
1.1.192026-07-17sha256:39b5d2f3f128df8e…current key2 high, 8 medium, 20 lowSBOMreport
1.1.182026-07-05sha256:d64f30c5a63cb238…unsigned2 high, 8 medium, 20 lowSBOMreport
1.1.172026-07-04sha256:e348fe51231d3b22…unsigned2 high, 8 medium, 20 lowSBOMreport
1.1.162026-07-03sha256:e1aafff347762ecd…2025 key2 high, 8 medium, 20 lowSBOMreport
1.1.142026-06-06sha256:35e72588f5b85339…unsignednoneSBOMreport
1.1.132026-05-04sha256:f62a39d37797fb26…unsignednoneSBOMreport
1.1.122026-05-04sha256:9a7d666b80db7d4c…unsignednoneSBOMreport
1.1.112026-05-04sha256:b7fc62ea0800b804…unsignednoneSBOMreport
1.1.102026-05-02sha256:d11edfb8fdc4b731…unsignednoneSBOMreport
1.1.92026-05-01sha256:81ffa2ddca3a1791…unsignednoneSBOMreport
1.1.82026-05-01sha256:1d3b2e90fa43c981…unsignednoneSBOMreport
1.1.72026-05-01sha256:14b6c4de345e0d98…unsignednoneSBOMreport
1.1.62026-05-01sha256:7a7889ee0cae396a…unsignednoneSBOMreport
1.1.42026-04-13sha256:d11adb1dc6253fe6…unsignednoneSBOMreport
1.1.32026-04-12sha256:c3c8e4620e97aa34…unsigned2 critical, 6 high, 19 medium, 11 low, 4 unknownSBOMreport
1.1.22025-12-01sha256:61a798fc8ed75358…2025 keyn/aSBOMno report
1.1.12025-08-03sha256:4da86362d8a75497…2025 keyn/aSBOMno report

A row is written when a release is signed, so the newest row can be one behind the version in the footer.

Honest limits

  • Kyverno on the cluster checks the signature of every ghcr.io/lillevang image, but in audit mode. An unsigned image would be reported, not blocked.
  • The signatures are not in a transparency log. Trust rests on the key alone.
  • Not every release in the table carries a signature. The ones marked unsigned were pushed before signing was part of every release, or while the key moved into 1Password; they are still in the registry as they were.