fix(security): restart a companion whose image was rebuilt underneath it
A rebuilt image never reached a running companion. ensure_image_present rebuilds in place under the same tag, so the quadlet body is identical, write_if_changed reports no change, and enable_now is a no-op on a running service — the container keeps the old layers indefinitely. That is precisely how archi-dev-box kept serving the LND, FIPS, Electrs and Guardian screens on 0.0.0.0 after v1.7.123 rebuilt every one of those images to bind loopback: correct images on disk, three-day-old containers still running. Closing those ports needed a manual 'podman rm -f' per container, which no other node would ever get. Compare the running container's image ID against the built one and restart when they diverge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ea4c072183
commit
9b30daaf9c
@@ -214,10 +214,59 @@ pub async fn install_one(spec: &CompanionSpec) -> Result<()> {
|
||||
}
|
||||
// Start is idempotent — if already running, systemctl returns 0.
|
||||
quadlet::enable_now(&unit.service_name()).await?;
|
||||
|
||||
// A rebuilt image does NOT reach a container that is already running.
|
||||
// `ensure_image_present` rebuilds in place under the same tag, so the unit
|
||||
// body is byte-identical, `write_if_changed` reports no change, and
|
||||
// `enable_now` is a no-op on a running service — the container keeps the
|
||||
// old layers indefinitely. That is exactly how archi-dev-box kept serving
|
||||
// the LND, FIPS, Electrs and Guardian screens on 0.0.0.0 after v1.7.123
|
||||
// rebuilt every one of those images to bind loopback: the images were
|
||||
// correct on disk and the running containers were three days old
|
||||
// (2026-08-05). Compare image IDs and restart when they diverge.
|
||||
if let Some(running) = container_image_id(spec.name).await {
|
||||
if let Some(built) = image_id(&image).await {
|
||||
if running != built {
|
||||
info!(
|
||||
companion = spec.name,
|
||||
"running container uses a stale image; restarting onto the rebuilt one"
|
||||
);
|
||||
quadlet::restart_service(&unit.service_name()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(companion = spec.name, "companion started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Image ID a container is actually running, or `None` when it does not exist.
|
||||
async fn container_image_id(name: &str) -> Option<String> {
|
||||
let out = tokio::process::Command::new("podman")
|
||||
.args(["inspect", name, "--format", "{{.Image}}"])
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let id = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
(!id.is_empty()).then_some(id)
|
||||
}
|
||||
|
||||
/// Current ID behind an image reference, or `None` when absent.
|
||||
async fn image_id(image_ref: &str) -> Option<String> {
|
||||
let out = tokio::process::Command::new("podman")
|
||||
.args(["image", "inspect", image_ref, "--format", "{{.Id}}"])
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let id = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
(!id.is_empty()).then_some(id)
|
||||
}
|
||||
|
||||
/// Build companion image locally if a Dockerfile exists, otherwise
|
||||
/// pull from the lfg2025 registry. Returns the image ref the quadlet
|
||||
/// should reference (`localhost/<base>:latest` for build, registry
|
||||
|
||||
Reference in New Issue
Block a user