v0.4.0
Signedpublished August 14, 2026What changed
Trust and polish for the install path (INSTALL_UPGRADE_PLAN.md phase 4), and
the first release signed with a key installers can verify against.
Added
- DNS is checked before the stack starts.
install.shresolves the dashboard hostname and a random label under the wildcard domain, compares them against this host's addresses, and says so before Let's Encrypt burns a failed challenge against a name that does not reach you. It asks rather than refusing — split-horizon DNS and a pre-cutover install are legitimate — and--skip-dns-checkturns it off. --dry-runreports what an upgrade would change: release, image and asset versions, download size against free disk in both filesystems, which settings would change, and whether a snapshot would be taken. It writes nothing.--yesfor unattended runs: never prompts, takes the prior value for every question. It deliberately does not answer the post-failure rollback question — that is what--auto-rollbackis for.- A concurrency lock. Two installers at once raced on
.env,versions.env,config.yamlanddocker compose up; the second now waits and then refuses. - Release signing.
make publishcan sign a release with a key that is not on the web host (scripts/release-key.shgenerates one), andinstall.shverifies it against a key embedded in the script, then refuses any artifact whose digest is not in the signed payload. Until now the only integrity check was a digest served by the same origin as the bytes, which catches corruption but not a compromised registry. Unverifiable releases warn and install;DEVGRAIL_REQUIRE_SIGNATURE=1makes them a refusal. The trust model, the signed payload format, and the key rotation procedure are documented indocs/SECURITY.md. docker-compose.override.ymlis supported. The compose file and Traefik config are product-owned and rewritten on every run; local changes belong in the override, which the installer never touches and now passes to every compose invocation. Documented indocs/RUNBOOK.md.README.mdwith an OS/arch support matrix and minimum RAM/disk, and aLICENSE.- A test asserting migration numbers stay unique, so the collision that exists
today (
005_api_keys/005_projects) cannot be joined by another.
Changed
- The registry's "current release" no longer depends on scan order when two releases are published in the same millisecond.
- The admin dashboard marks each release as signed or unsigned.
install.shinstallsopensslalongsidejqwhen it is missing.- The VPS marketplace / cloud-init packaging described in the spec is now marked explicitly as not built.
- Publishing a release is one command:
scripts/publish.shtakes the version from this file, finds the registry URL, tokens and signing key on its own, and runs on Windows as well as Linux.make publishis unchanged.
Fixed
- arm64 workspaces get a terminal that runs. The workspace image fetched the
x86_64 ttyd binary whatever it was being built for. The pinned digest matched
(it is the same file), so the build passed and the breakage only appeared on a
customer's arm64 host, where the terminal could not start. The binary is now
chosen by
TARGETARCH, with a pinned digest per architecture, and an unrecognised architecture fails the build instead of shipping.
Container images
Installer files
Signature
ecdsa-p256-sha256key f1f1eaf642454d65
One signature covers every artifact above. It is made off this host, and neither this site nor the registry behind it can check it — that is the point, and it is why verifying it catches things a checksum from the same server cannot. How to verify it.
Verify devgrail-container (amd64)
curl -fsSL https://web-dev.trixibot.com/api/registry/releases/v0.4.0 -o release.json
# -j, not -r: -r appends a newline the signer never saw, and the check fails.
jq -j '.signature.payload' release.json > payload
jq -r '.signature.signature' release.json | base64 -d > payload.sig
openssl dgst -sha256 -verify devgrail-release.pem -signature payload.sig payload
# -> Verified OK
# The payload must name this release, so an older signed one cannot be replayed.
head -2 payload
# ...and it must name this exact file. -qxF: whole line, literal.
grep -qxF 'image devgrail-container 8 amd64 f061201afa934a2a14c282492afdeef7331f3310fa3b605be04279ca367a73f0' payload && echo "digest is covered by the signature"The signed payload — shown for reading, not for verifying
Copy this from the manifest with jq -j, never from this page: the signature covers exact bytes, and rendering here drops the trailing newline the signer included.
devgrail-release-signature-v1
release v0.4.0
asset docker-compose.yml 7 d716c3cd9ffd1f4a87caacb8d1b444646f9cf388949d8c2805d2676981307abc
asset install.sh 7 4890788c5c172561e228ce0c8183d81b37edeccd385765a28e45ff47c836c511
asset traefik-dynamic-http.yml 6 e344c8efd13c7d84996e2ebfbc75dcee2d995d8538c8a245c89c0e44fad0089b
asset traefik-dynamic.yml 7 d18fa561b8eb7d53b015284e0246b2d05953bf281b65269c1fae3d6883530db6
asset uninstall.sh 7 d8f09fe906e13f57bc308f6f2340972a62b010c367174ec79597c86a84c706be
image devgrail-container 8 amd64 f061201afa934a2a14c282492afdeef7331f3310fa3b605be04279ca367a73f0
image devgrail-container 8 arm64 96446a08986a283710a4c686c0f245d90f2ce52202ed8ebbd34f398732f8e208
image devgrail-server 8 amd64 76ecbcb59368ebd4d809876aea9ab3a82668bef79900f2c974700b6362b4dc47
image devgrail-server 8 arm64 2c61596f68f278d2039fe7c56a74c3ead151904be862f50efe14913bda784819