Files
roro9stack/site/content/dev/decisions/0008-ci-signs-releases.md
T
twislaandClaude Sonnet 5.5 3b4100dc6a
CI / build (pull_request) Successful in 8m38s
Site / build (pull_request) Successful in 9s
Site: the developer docs (phase 4), with the Debug Builds and the Debug Console first
/dev/ has Debug Builds and the Debug Console (builds and the token, the
console and its protocol, files and screenshots, driving the UI, crashes and
Safe Mode, the command reference), Build, test and release (including how an
update works), the architecture decisions and the milestone plans.

Generated from the repository by site/tools/gen_dev_docs.py: the ADRs, the
milestones, the README's sections, and the command reference, read from the
firmware's own `help` text. The pages are committed (Zola cannot read outside
its folder); the Site workflow checks they are current, and now also runs
when src/main.cpp changes. M0, M1 and CONTEXT.md are not published.
README: the gnss commands that the table lacked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 21:25:33 +02:00

2.2 KiB

+++ title = "CI signs releases with the project's key" description = "A tag v is built, signed and published by Gitea Actions with nobody at a keyboard. The signing key of ADR 0003 is therefore held twice: in ~/.config/roro9stack/ota-key.pem on the development machine, as before, and as the…" weight = 8

[extra] docs = true source = "docs/adr/0008-ci-signs-releases.md" tag = "ADR 0008" +++ A tag v* is built, signed and published by Gitea Actions with nobody at a keyboard. The signing key of ADR 0003 is therefore held twice: in ~/.config/roro9stack/ota-key.pem on the development machine, as before, and as the repository secret OTA_SIGNING_KEY, which the release step writes to a file for as long as it runs.

We chose this over signing by hand after CI has built (a command per release, the key in one place), and over a second key for CI that the firmware would also trust. A release that needs a manual step isn't made on the day it's ready, and issue #6, the device installing releases by itself, needs releases that are always there and always signed.

What it costs

  • Whoever can run a workflow in this repository can sign firmware every device accepts. That means: anyone who can push to it, the runner's host and whoever administers it, and the Gitea instance with its database, where the secret is stored. Before, it took the development machine.
  • The runner executes jobs on its own host, not in a container, as a user who can use Docker. A workflow is not confined.
  • Pull requests from forks must never run with this secret. Gitea doesn't pass secrets to them; the workflow also only runs on pushes and by hand.

What limits it

  • The release step checks the signed file against the public key in the sources it built (scripts/ota_verify.py): a wrong or replaced secret stops the release instead of publishing a file no device takes.
  • ADR 0003's way out stays: a firmware release can carry a new public key. If the secret is ever in doubt, make a new pair, ship it in a release signed with the old key, and replace the secret.
  • A device still only installs what it's told to (until #6), keeps a new image on Probation, and rolls back one that doesn't hold.