From 94b84b7894d1bb6f21e93cd61fda3f793363dba8 Mon Sep 17 00:00:00 2001 From: Kunal Mehta Date: Thu, 14 Nov 2024 18:58:29 -0500 Subject: [PATCH] WIP: Script to upgrade from focal to noble The script is split into various stages where progress is tracked on-disk. The script is able to resume where it was at any point, and needs to, given multiple reboots in the middle. Fixes #7332. --- noble-migration/files/apt_freedom_press.list | 1 + noble-migration/files/sources.list | 13 + noble-migration/src/bin/upgrade.rs | 301 +++++++++++++++++++ securedrop/debian/rules | 3 +- 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 noble-migration/files/apt_freedom_press.list create mode 100644 noble-migration/files/sources.list create mode 100644 noble-migration/src/bin/upgrade.rs diff --git a/noble-migration/files/apt_freedom_press.list b/noble-migration/files/apt_freedom_press.list new file mode 100644 index 0000000000..12e72a6778 --- /dev/null +++ b/noble-migration/files/apt_freedom_press.list @@ -0,0 +1 @@ +deb [arch=amd64] https://apt.freedom.press noble main diff --git a/noble-migration/files/sources.list b/noble-migration/files/sources.list new file mode 100644 index 0000000000..df96219dc8 --- /dev/null +++ b/noble-migration/files/sources.list @@ -0,0 +1,13 @@ +## newer versions of the distribution. +deb http://archive.ubuntu.com/ubuntu/ noble main + +## newer versions of the distribution. +deb http://archive.ubuntu.com/ubuntu/ noble universe + +## Major bug fix updates produced after the final release of the +## distribution. +deb http://archive.ubuntu.com/ubuntu/ noble-updates main + +### Security fixes for distribution packages +deb http://security.ubuntu.com/ubuntu noble-security main +deb http://security.ubuntu.com/ubuntu noble-security universe diff --git a/noble-migration/src/bin/upgrade.rs b/noble-migration/src/bin/upgrade.rs new file mode 100644 index 0000000000..65cf00f752 --- /dev/null +++ b/noble-migration/src/bin/upgrade.rs @@ -0,0 +1,301 @@ +//! Migrate a SecureDrop server from focal to noble +//! +//! This script should never be run directly, only via the +//! systemd service. +use anyhow::{anyhow, Result}; +use rustix::process::geteuid; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs::{self, Permissions}, + os::unix::fs::PermissionsExt, + process::{Command, ExitCode}, + thread::sleep, + time::Duration, +}; + +const STATE_PATH: &str = "/etc/securedrop-noble-migration.json"; + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +enum Stage { + None, + PendingUpdates, + MigrationCheck, + Backup, + SuspendOSSEC, + DisableUnattendedUpdates, + ChangeAptSources, + AptGetUpdate, + AptGetUpgradeNoNew, + AptGetFullUpgrade, + ReenableUnattendedUpdates, + ReenableOSSEC, + Reboot, + SwitchUbuntuSources, + IntegrityCheck, + RemoveBackup, + Done, +} + +#[derive(Serialize, Deserialize)] +struct State { + finished: Stage, +} + +impl State { + fn load() -> Result { + if !fs::exists(STATE_PATH)? { + return Ok(State { + finished: Stage::None, + }); + } + // FIXME: what if this fails? + Ok(serde_json::from_str(&fs::read_to_string(STATE_PATH)?)?) + } + + fn set(&mut self, stage: Stage) -> Result<()> { + self.finished = stage; + self.save() + } + + fn save(&self) -> Result<()> { + fs::write(STATE_PATH, serde_json::to_string(self)?)?; + Ok(()) + } +} + +fn run_next_stage(state: &mut State) -> Result<()> { + match state.finished { + Stage::None => { + pending_updates(state)?; + state.set(Stage::PendingUpdates)?; + Ok(()) + } + Stage::PendingUpdates => { + migration_check()?; + state.set(Stage::MigrationCheck)?; + Ok(()) + } + Stage::MigrationCheck => { + backup()?; + state.set(Stage::Backup)?; + Ok(()) + } + Stage::Backup => { + suspend_ossec()?; + state.set(Stage::SuspendOSSEC)?; + Ok(()) + } + Stage::SuspendOSSEC => { + disable_unattended_updates()?; + state.set(Stage::DisableUnattendedUpdates)?; + Ok(()) + } + Stage::DisableUnattendedUpdates => { + change_apt_sources()?; + state.set(Stage::ChangeAptSources)?; + Ok(()) + } + Stage::ChangeAptSources => { + apt_get_update()?; + state.set(Stage::AptGetUpdate)?; + Ok(()) + } + Stage::AptGetUpdate => { + apt_get_upgrade_no_new()?; + state.set(Stage::AptGetUpgradeNoNew)?; + Ok(()) + } + Stage::AptGetUpgradeNoNew => { + apt_get_full_upgrade()?; + state.set(Stage::AptGetFullUpgrade)?; + Ok(()) + } + Stage::AptGetFullUpgrade => { + reenable_unattended_updates()?; + state.set(Stage::ReenableUnattendedUpdates)?; + Ok(()) + } + Stage::ReenableUnattendedUpdates => { + reenable_ossec()?; + state.set(Stage::ReenableOSSEC)?; + Ok(()) + } + Stage::ReenableOSSEC => { + reboot(state)?; + state.set(Stage::Reboot)?; + Ok(()) + } + Stage::Reboot => { + switch_ubuntu_sources()?; + state.set(Stage::SwitchUbuntuSources)?; + Ok(()) + } + Stage::SwitchUbuntuSources => { + integrity_check()?; + state.set(Stage::IntegrityCheck)?; + Ok(()) + } + Stage::IntegrityCheck => { + remove_backup()?; + state.set(Stage::RemoveBackup)?; + Ok(()) + } + Stage::RemoveBackup => { + state.set(Stage::Done)?; + Ok(()) + } + Stage::Done => Ok(()), + } +} + +/// A wrapper to roughly implement Python's subprocess.check_call/check_output +fn check_call(binary: &str, args: &[&str]) -> Result { + let output = Command::new(binary).args(args).output()?; + + if !output.status.success() { + eprintln!("{}", String::from_utf8_lossy(&output.stderr)); + return Err(anyhow!("{} failed", binary)); + } + Ok(String::from_utf8(output.stdout)?) +} + +fn pending_updates(state: &mut State) -> Result<()> { + check_call("apt-get", &["update"])?; + check_call("unattended-upgrade", &[])?; + state.set(Stage::PendingUpdates)?; + check_call("systemctl", &["reboot"])?; + Ok(()) +} + +fn migration_check() -> Result<()> { + // TODO: consider just invoking the Rust code directly + check_call("systemctl", &["start", "securedrop-noble-migration-check"])?; + loop { + // Wait for the service to finish + let output = check_call( + "systemctl", + &["is-active", "securedrop-noble-migration-check"], + )?; + if output == "active" { + sleep(Duration::from_secs(1)); + } else { + break; + } + } + let data: HashMap = serde_json::from_str( + &fs::read_to_string("/etc/securedrop-noble-migration-check.json")?, + )?; + if data.contains_key("error") { + return Err(anyhow!("Migration check errored")); + } else if data.values().any(|val| val == &false) { + return Err(anyhow!("Migration check failed")); + } + + Ok(()) +} + +fn backup() -> Result<()> { + // Create a root-only directory to store the backup + fs::create_dir("/var/lib/securedrop-backups")?; + let permissions = Permissions::from_mode(0o700); + fs::set_permissions("/var/lib/securedrop-backups", permissions)?; + check_call( + "/usr/bin/securedrop-app-backup.py", + &["--dest", "/var/lib/securedrop-backups"], + )?; + Ok(()) +} + +fn suspend_ossec() -> Result<()> { + // FIXME: edit ossec.conf + // FIXME: check we're on the mon server + check_call("systemctl", &["restart", "ossec"])?; + Ok(()) +} + +fn disable_unattended_updates() -> Result<()> { + // FIXME: apt-daily timers + check_call("systemctl", &["mask", "unattended-upgrades"])?; + Ok(()) +} + +fn change_apt_sources() -> Result<()> { + fs::write( + "/etc/apt/sources.list", + include_str!("../../files/sources.list"), + )?; + fs::write( + "/etc/apt/sources.list.d/apt_freedom_press.list", + include_str!("../../files/apt_freedom_press.list"), + )?; + Ok(()) +} + +fn apt_get_update() -> Result<()> { + check_call("apt-get", &["update"])?; + Ok(()) +} + +fn apt_get_upgrade_no_new() -> Result<()> { + check_call("apt-get", &["upgrade", "--without-new-pkgs"])?; + Ok(()) +} + +fn apt_get_full_upgrade() -> Result<()> { + check_call("apt-get", &["full-upgrade"])?; + Ok(()) +} + +fn reenable_unattended_updates() -> Result<()> { + // FIXME: apt-daily timers + check_call("systemctl", &["unmask", "unattended-upgrades"])?; + Ok(()) +} + +fn reenable_ossec() -> Result<()> { + // FIXME: edit ossec.conf + // FIXME: check we're on the mon server + check_call("systemctl", &["restart", "ossec"])?; + Ok(()) +} + +fn reboot(state: &mut State) -> Result<()> { + state.set(Stage::Reboot)?; + check_call("systemctl", &["reboot"])?; + Ok(()) +} + +fn switch_ubuntu_sources() -> Result<()> { + // FIXME: do something here + Ok(()) +} + +fn integrity_check() -> Result<()> { + // FIXME: do something here + // FIXME: check no failing systemd units + // FIXME: check firewall is up + Ok(()) +} + +fn remove_backup() -> Result<()> { + fs::remove_dir_all("/var/lib/securedrop-backups")?; + Ok(()) +} + +fn main() -> Result { + if !geteuid().is_root() { + println!("This script must be run as root"); + return Ok(ExitCode::FAILURE); + } + + let mut state = State::load()?; + loop { + run_next_stage(&mut state)?; + if state.finished == Stage::Done { + break; + } + } + + Ok(ExitCode::SUCCESS) +} diff --git a/securedrop/debian/rules b/securedrop/debian/rules index c30672f1c7..b3f846b1fb 100755 --- a/securedrop/debian/rules +++ b/securedrop/debian/rules @@ -22,7 +22,8 @@ override_dh_auto_install: cd /srv/rust/noble-migration && cargo build --release --locked && \ cd /srv/securedrop && \ mkdir -p ./debian/securedrop-config/usr/bin && \ - mv /srv/rust/target/release/check ./debian/securedrop-config/usr/bin/securedrop-noble-migration-check + mv /srv/rust/target/release/check ./debian/securedrop-config/usr/bin/securedrop-noble-migration-check && \ + mv /srv/rust/target/release/upgrade ./debian/securedrop-config/usr/bin/securedrop-noble-upgrade # Build redwood wheel python3 /srv/rust/redwood/build-wheel.py --release --redwood /srv/rust/redwood --target /srv/rust/target # Set up virtualenv and install dependencies