A helper that would have deployed a channel nobody named

The channel is declared twice: in lembas.env, which this process reads and the
page prints, and baked into the systemd unit, which is what the helper actually
deploys. install.sh writes both together so they agree by construction -- and
the moment somebody edits one by hand they diverge, with the page naming one
channel down every card and the button deploying the other. Nothing anywhere
would have said so.

It cannot be collapsed to one place. Reading it from lembas.env at deploy time
would mean the service account decides what gets deployed, since it owns that
file -- and "the request carries no channel" is the property the whole design
rests on. So the two stay, and the marker file the page already reads to know
the helper exists now carries the channel it was installed with. A disagreement
is an alert.

Display only, deliberately: the service account can write that marker, so a
compromised process could lie about what the helper will do -- but not change
it, because the helper's own channel lives in /etc where that account cannot
reach. Lying about the channel is a much smaller thing than choosing it.

An empty marker -- every host installed before this -- reads as unknown rather
than as a mismatch. Claiming one would put a red alert on every existing host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 21:44:35 +02:00
parent f68caec849
commit 0ce8026bd2
5 changed files with 98 additions and 4 deletions
+37
View File
@@ -101,6 +101,19 @@ REQUEST_NAME = "update-requested"
# Written by `install.sh` when the helper is installed. A marker rather than
# asking systemd, because `systemctl is-enabled` means a subprocess on every
# page render to answer a question that changes once.
#
# It carries the channel the helper was installed with, because that is declared
# in **two** places -- here for the helper, and `lembas.env` for this process --
# and `install.sh` writing both together is the only thing keeping them in step.
# Edit one by hand and the page says `stable` while the button deploys `edge`,
# with nothing anywhere disagreeing. Reading it back is what lets the page say
# so. Empty on every host installed before this, which reads as "unknown" rather
# than as a mismatch.
#
# Display only. The service account can write this file, so a compromised
# process could lie about what the helper will do -- but not change it, because
# the helper's own channel is baked into its unit under /etc, which this account
# cannot touch. Lying about the channel is a much smaller thing than choosing it.
MARKER_NAME = ".update-helper"
# `%H` sha, `%s` subject, `%cI` date, split on a unit separator rather than a
@@ -159,9 +172,23 @@ class State:
# before the version bump names a release nobody can identify afterwards.
version_mismatch: str = ""
helper: bool = False
# What the helper will actually deploy, when it said. Empty means it did not
# -- an older install -- and the page then says nothing rather than claiming
# agreement it cannot check.
helper_channel: str = ""
requested: bool = False
error: str = ""
@property
def channel_mismatch(self) -> bool:
"""The page and the helper disagree about what would be deployed.
Only answerable when the helper said which channel it has. Somebody
edited one of the two places and not the other, and without this the
button would quietly deploy something other than what the page named.
"""
return bool(self.helper_channel) and self.helper_channel != self.channel
@property
def is_git(self) -> bool:
return bool(self.checkout)
@@ -239,6 +266,14 @@ def helper_installed() -> bool:
return (settings.data_dir / MARKER_NAME).exists()
def helper_channel() -> str:
"""The channel the helper was installed with, or "" if it did not say."""
try:
return (settings.data_dir / MARKER_NAME).read_text(encoding="utf-8").strip()
except OSError:
return ""
def request_path() -> Path:
return settings.data_dir / REQUEST_NAME
@@ -345,6 +380,7 @@ def read(*, fetch: bool = False) -> State:
"channel": channel,
"branch": branch,
"helper": helper_installed(),
"helper_channel": helper_channel(),
"requested": pending(),
}
if root is None:
@@ -444,6 +480,7 @@ __all__ = [
"channel_name",
"checkout_dir",
"clear_request",
"helper_channel",
"helper_installed",
"manual_command",
"pending",