Risks: releases and updating
Known hazards in how Layover is released and how it updates itself. Part of the risks, whose numbering this file shares.
14. Release pipeline trust
Severity: low. Mostly mitigated.
The release workflow holds contents: write and the Pages workflow holds id-token: write. Any
action running in those jobs runs with those tokens, so a compromised upstream would be a
release-pipeline compromise.
Mitigation: .github/workflows/release.yml is generated by dist
and uses only first-party actions/* — the three third-party actions the hand-rolled workflow
needed are gone, which was the larger exposure. In ci.yml and pages.yml, which are still
hand-written, third-party actions are pinned to full commit SHAs.
Residual, accepted: the generated workflow interpolates github.ref_name into one run: block,
which is the injection shape the hand-rolled workflow was fixed for. Exploiting it needs the
ability to push a v* tag — the workflow never runs on pull_request — so it is a
privileged-user issue rather than an external one. It is not patched locally: the file is
generated, dist plan would report a hand-edit as drift, and diverging from upstream to fix a
low-severity issue costs more than it saves. Revisit if dist addresses it upstream.
Residual: actions/* are pinned to major tags, which is the usual trade-off.
26. Layover now makes a request of its own
What could happen. Every running factory with [update] mode = "notify" — the default — asks
github.com once a day for the newest release's manifest. That request carries the machine's address
and a User-Agent of layover/<version>: GitHub learns that a Layover of that version runs there. On
a network that must not reach out, it is a request nobody expected, and a proxy may log or refuse it.
What is read back steers nothing yet — it is only shown — but a manifest is input from the network,
and the next step, applying a release, makes it load-bearing.
Why it is accepted. The alternative is a factory that runs a release with a known fault for weeks because nobody looked; the request names nothing about the factory, its agents or its work. It is one request for a public file, on github.com rather than the rate-limited API, so a building of factories behind one NAT does not spend each other's budget. No telemetry is sent, and none is planned while the maintainer is the only user.
Mitigation. LAYOVER_UPDATE=off stops every request on a machine, whatever a factory says, and
[update] mode = "off" stops one factory's. A failed check is retried with back-off and never fails
a run, a start or a validation. The manifest is parsed with every field optional and size-capped at
2 MB; a manifest naming no version is refused; nothing in it is executed or rendered as HTML.
LAYOVER_UPDATE_SOURCE points a site at a mirror it controls.
27. A release's provenance is only as good as gh and its pins
What could happen. A staged release is trusted to run, and in time to install, because gh attestation verify said Layover's release workflow built it from its tag. If gh were replaced on
PATH by something that always exits 0, every archive would read as verified. If the pins were
wrong — another repository's workflow, another tag — a release built elsewhere could pass. And on a
machine without gh, nothing is verified at all.
Why it is accepted. The alternative, verifying inside the binary, was tried and the library
refused genuine bundles and skipped the transparency-log proof (see the decision log). gh is
GitHub's own verifier, and something that can replace it on PATH can already replace layover.
Mitigation. The pins are constants, tested: repository, signer workflow, refs/tags/<tag>,
issuer and predicate. The archive must also match the manifest's SHA-256 and the .sha256 file, and
the binary must then say it is the version and platform it should be. A release whose provenance
could not be checked is downloaded but never run, never installed unattended, and installed by hand
only with --trust-checksum-only, which is said in the record.
28. Installing a release restarts the Tower, and a release can fail in ways a test does not
What could happen. An install cuts off any run still alive at [update] max_drain, and a run
cut off halfway through an external effect repeats it when recovery restarts it — the hazard of
risk 17, at a moment a person chose rather than a crash. A release can pass every check and still
be wrong: healthy by the Tower's measure, and failing runs that used to pass. Under systemd or
launchd, a new binary that dies before it reads the handover never counts a start, so it is never
rolled back automatically; the service manager restarts it until it gives up. And a Tower started by
an update writes what it prints to .layover/serve.log, not to the terminal or log the old one
wrote to, which a person looking in the old place will not find.
Why it is accepted. Not updating is the larger hazard for a factory nobody watches, and every
one of these is bounded: the deadline waits out the longest timeout by default, recovery = "manual" agents postpone it rather than being cut off, nothing installs without a person asking,
and nothing is installed that was not verified and did not validate the factory.
Mitigation. The replaced binary is kept for keep_previous (a week), and layover update rollback puts it back. On Windows the old Tower supervises the new one and rolls back a release
that does not answer within two minutes, at once. doctor says when a rollback happened in the last
thirty days, and layover update log keeps every install, refusal, postponement and rollback.
29. An unattended install takes a release nobody has looked at
What could happen. Under [update] mode = "auto", a factory installs a patch or minor release
by itself. A release can be wrong in a way no check here sees — healthy, validating the factory, and
quietly worse at the work — and every unattended factory takes it once its cool-off has passed.
Why it is accepted. It is opt-in, per factory: the default only tells. And a factory nobody watches that never updates runs on known faults.
Mitigation. The cool-off — a day for a patch, two for a minor — is the time to withdraw a bad
release: marking it a pre-release on GitHub moves releases/latest back, and a fixed patch follows.
A major release, a layout change, an unverified release and one that gives a new warning all wait
for a person. Installs happen only in the window chosen, or when the factory has been idle. Every
one is logged, the bell rings for it, the previous binary is kept a week, and layover update rollback puts it back.