fix(security): disable fail-open auto-updater until Phase 4
Gate the nunciod 24h auto-update-check loop behind NUNCIO_AUTO_UPDATE_ENABLED (defaults off), so the daemon never autonomously checks/installs updates. Make update.apply RPC return an explicit 'disabled pending Phase 4' error instead of performing the fail-open install; update.check (read-only) is preserved. UpdateEngine (update.rs) untouched for the Phase 4 checksum fix. Refs #140
This commit is contained in:
2 files changed
+104
-36
No files matched your search
@@ -4,3 +4,61 @@
|
||||
|
||||
pub mod orchestrator;
|
||||
pub use orchestrator::SelfHealingSyncOrchestrator;
|
||||
|
||||
/// Environment variable that opts `nunciod` in to its autonomous
|
||||
/// background auto-update-check loop.
|
||||
///
|
||||
/// # Security (GH #140 / backlog story 0.B.2)
|
||||
///
|
||||
/// `nuncio_core::UpdateEngine`'s checksum verification is currently
|
||||
/// **fail-open**: if the `SHA256SUMS.txt` release asset is missing, an
|
||||
/// update install proceeds without checksum verification. That will be
|
||||
/// fixed properly in Phase 4. Until then, `nunciod` MUST NOT
|
||||
/// autonomously check for or install updates, so the background loop
|
||||
/// defaults to disabled and is only spawned when an operator explicitly
|
||||
/// opts in via this environment variable.
|
||||
pub const AUTO_UPDATE_ENV_VAR: &str = "NUNCIO_AUTO_UPDATE_ENABLED";
|
||||
|
||||
/// Returns `true` only if the operator has explicitly opted in to the
|
||||
/// autonomous background auto-update-check loop by setting
|
||||
/// [`AUTO_UPDATE_ENV_VAR`] to `"1"` or `"true"` (case-insensitive).
|
||||
/// Defaults to `false` (disabled).
|
||||
///
|
||||
/// See the module-level documentation on [`AUTO_UPDATE_ENV_VAR`] for why
|
||||
/// this defaults to disabled.
|
||||
pub fn auto_update_task_enabled() -> bool {
|
||||
match std::env::var(AUTO_UPDATE_ENV_VAR) {
|
||||
Ok(val) => val.eq_ignore_ascii_case("1") || val.eq_ignore_ascii_case("true"),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Serialize access to the shared process environment variable so this
|
||||
// test cannot interleave with any other test that might touch it.
|
||||
static ENV_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn auto_update_task_disabled_by_default() {
|
||||
let _guard = ENV_GUARD.lock().unwrap_or_else(|e| e.into_inner());
|
||||
std::env::remove_var(AUTO_UPDATE_ENV_VAR);
|
||||
assert!(
|
||||
!auto_update_task_enabled(),
|
||||
"autonomous auto-update loop must default to disabled (GH #140)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_update_task_enabled_requires_explicit_opt_in() {
|
||||
let _guard = ENV_GUARD.lock().unwrap_or_else(|e| e.into_inner());
|
||||
std::env::set_var(AUTO_UPDATE_ENV_VAR, "true");
|
||||
assert!(auto_update_task_enabled());
|
||||
std::env::set_var(AUTO_UPDATE_ENV_VAR, "0");
|
||||
assert!(!auto_update_task_enabled());
|
||||
std::env::remove_var(AUTO_UPDATE_ENV_VAR);
|
||||
assert!(!auto_update_task_enabled());
|
||||
}
|
||||
}
|
||||
+46
-36
@@ -52,27 +52,43 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
}
|
||||
});
|
||||
|
||||
// Background Auto-Update Check Listener Loop (24h interval)
|
||||
let event_bus_update = event_bus.clone();
|
||||
let _update_task = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(86400));
|
||||
interval.tick().await;
|
||||
loop {
|
||||
// Background Auto-Update Check Listener Loop (24h interval).
|
||||
//
|
||||
// SECURITY (GH #140 / backlog story 0.B.2): `UpdateEngine`'s checksum
|
||||
// verification is currently fail-open (installs proceed unverified if
|
||||
// `SHA256SUMS.txt` is missing from the release). That will be fixed
|
||||
// properly in Phase 4. Until then, nunciod must never autonomously
|
||||
// check for or install updates, so this loop only runs when an
|
||||
// operator explicitly opts in via `NUNCIO_AUTO_UPDATE_ENABLED=1`.
|
||||
let _update_task = if nunciod::auto_update_task_enabled() {
|
||||
let event_bus_update = event_bus.clone();
|
||||
Some(tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(86400));
|
||||
interval.tick().await;
|
||||
if let Ok(updater) = nuncio_core::UpdateEngine::new() {
|
||||
if let Ok(result) = updater.check_for_updates().await {
|
||||
if result.update_available {
|
||||
if let Some(info) = result.release_info {
|
||||
event_bus_update.publish_event(CoreEvent::UpdateAvailable {
|
||||
version: info.version,
|
||||
release_notes: info.release_notes,
|
||||
});
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Ok(updater) = nuncio_core::UpdateEngine::new() {
|
||||
if let Ok(result) = updater.check_for_updates().await {
|
||||
if result.update_available {
|
||||
if let Some(info) = result.release_info {
|
||||
event_bus_update.publish_event(CoreEvent::UpdateAvailable {
|
||||
version: info.version,
|
||||
release_notes: info.release_notes,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}))
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Autonomous auto-update check loop disabled (set {} to enable); \
|
||||
pending Phase 4 checksum fail-open fix, see GH #140",
|
||||
nunciod::AUTO_UPDATE_ENV_VAR
|
||||
);
|
||||
None
|
||||
};
|
||||
|
||||
// Custom RPC Handler for filter.* and update.* methods
|
||||
|
||||
@@ -243,26 +259,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
},
|
||||
Err(e) => Some(Err(e.to_string())),
|
||||
},
|
||||
"update.apply" => match nuncio_core::UpdateEngine::new() {
|
||||
Ok(updater) => match updater.check_for_updates().await {
|
||||
Ok(res) => {
|
||||
if let Some(info) = res.release_info {
|
||||
match updater.apply_update(&info).await {
|
||||
Ok(msg) => {
|
||||
Some(Ok(json!({ "status": "updated", "message": msg })))
|
||||
}
|
||||
Err(e) => Some(Err(e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Some(Ok(
|
||||
json!({ "status": "already_up_to_date", "message": "Already up to date" }),
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => Some(Err(e.to_string())),
|
||||
},
|
||||
Err(e) => Some(Err(e.to_string())),
|
||||
},
|
||||
"update.apply" => {
|
||||
// SECURITY (GH #140 / backlog story 0.B.2): `UpdateEngine`'s
|
||||
// checksum verification is currently fail-open (an update
|
||||
// installs unverified if `SHA256SUMS.txt` is missing from
|
||||
// the release). Until that is fixed in Phase 4, nunciod
|
||||
// must never perform an on-demand install either. Use
|
||||
// `update.check` for a read-only version check, and
|
||||
// install updates manually in the meantime.
|
||||
Some(Err(
|
||||
"auto-update is disabled pending Phase 4 (fail-open checksum fix, \
|
||||
see GH #140); install updates manually for now"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user