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:
+9
-1
@@ -16,7 +16,15 @@ for 1.0.0 have something to be assembled from.
|
||||
|
||||
## Unreleased
|
||||
|
||||
Nothing since 0.9.8.
|
||||
- The Updates page notices when the update helper on a host was installed for a
|
||||
**different channel** than the page follows. It is declared in two places —
|
||||
`lembas.env` and the systemd unit — and only the installer writes both, so
|
||||
editing one by hand would have left the button deploying something other than
|
||||
what the page named, with nothing anywhere saying so.
|
||||
- Fixed: release notes from a **signed** tag rendered the signature block.
|
||||
`_notes_for` stripped the PGP header only, and which header appears depends on
|
||||
`gpg.format` — this repository signs with SSH.
|
||||
- A `CHANGELOG.md`, kept from now on rather than assembled at release time.
|
||||
|
||||
## 0.9.8
|
||||
|
||||
|
||||
+4
-1
@@ -175,7 +175,10 @@ if [[ "$INSTALL_UPDATE_HELPER" == "1" ]]; then
|
||||
done
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now lembas-update.path
|
||||
sudo touch "$UPDATE_MARKER"
|
||||
# The channel goes *into* the marker, not just its existence. It is declared
|
||||
# in two places -- the unit above and lembas.env -- and this is what lets the
|
||||
# Updates page notice when somebody has edited one and not the other.
|
||||
echo "$CHANNEL" | sudo tee "$UPDATE_MARKER" >/dev/null
|
||||
sudo chown "$SERVICE_USER:$SERVICE_USER" "$UPDATE_MARKER"
|
||||
echo " installed. The web interface can now deploy the $CHANNEL channel and restart."
|
||||
else
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -181,6 +181,25 @@
|
||||
</form>
|
||||
|
||||
{% elif state.helper %}
|
||||
{% if state.channel_mismatch %}
|
||||
{#
|
||||
The two declarations have drifted. Worth its own alert rather than a note:
|
||||
the button would deploy something other than what this page has been naming
|
||||
all the way down, and nothing else anywhere would say so.
|
||||
#}
|
||||
<div class="alert alert--error">
|
||||
{{ icon("warning", "icon--sm") }}
|
||||
<span>
|
||||
This page follows <strong>{{ state.channel }}</strong> but the update
|
||||
helper on this host was installed for
|
||||
<strong>{{ state.helper_channel }}</strong> — so the button would deploy
|
||||
the second, not the first. They are set in two places
|
||||
(<code>lembas.env</code> and the systemd unit); re-run the installer with
|
||||
<code>INSTALL_UPDATE_HELPER=1</code> and the channel you want, which
|
||||
writes both.
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="card__lede">
|
||||
This host has the update helper installed, so the button below writes a
|
||||
request that a systemd unit picks up and runs as root. It always deploys the
|
||||
|
||||
+29
-2
@@ -45,8 +45,8 @@ def clean():
|
||||
updates.clear_request()
|
||||
|
||||
|
||||
def _install_helper():
|
||||
(settings.data_dir / updates.MARKER_NAME).touch()
|
||||
def _install_helper(channel: str = ""):
|
||||
(settings.data_dir / updates.MARKER_NAME).write_text(channel)
|
||||
|
||||
|
||||
# --- Reading the state ----------------------------------------------------------
|
||||
@@ -265,6 +265,33 @@ def test_an_unknown_channel_falls_back_to_stable(db, monkeypatch):
|
||||
assert updates.channel_name() == updates.CHANNEL_STABLE
|
||||
|
||||
|
||||
def test_a_helper_installed_for_another_channel_is_named(db, client, registered):
|
||||
"""The channel is declared in two places -- lembas.env and the systemd unit --
|
||||
and only the installer writes both. Edit one by hand and the button deploys
|
||||
something other than what the page has been naming all the way down."""
|
||||
_install_helper("stable" if updates.channel_name() == "edge" else "edge")
|
||||
|
||||
state = updates.read()
|
||||
assert state.channel_mismatch is True
|
||||
assert "would deploy" in client.get("/admin/updates").text
|
||||
|
||||
|
||||
def test_a_helper_that_did_not_say_claims_no_disagreement(db):
|
||||
"""Every host installed before the marker carried a channel. Unknown is not
|
||||
a mismatch, and claiming one would be a red alert on every existing host."""
|
||||
_install_helper("")
|
||||
|
||||
assert updates.helper_channel() == ""
|
||||
assert updates.read().channel_mismatch is False
|
||||
|
||||
|
||||
def test_a_matching_helper_says_nothing(db, client, registered):
|
||||
_install_helper(updates.channel_name())
|
||||
|
||||
assert updates.read().channel_mismatch is False
|
||||
assert "would deploy" not in client.get("/admin/updates").text
|
||||
|
||||
|
||||
# --- The page ---------------------------------------------------------------------
|
||||
def test_the_page_offers_nothing_without_the_helper(db, client, registered):
|
||||
page = client.get("/admin/updates").text
|
||||
|
||||
Reference in New Issue
Block a user