-
Notifications
You must be signed in to change notification settings - Fork 685
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a basic noble migration check script
Perform a number of checks to ensure the system is ready for the noble migration. The results are written to a JSON file in /etc/ that other things like the JI and the upgrade script itself can read from. The script is run hourly on a systemd timer but can also be run interactively for administrators who want slightly more details. Refs #7322.
- Loading branch information
Showing
10 changed files
with
1,371 additions
and
81 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
//! Check migration of a SecureDrop server from focal to noble | ||
//! | ||
//! This script is run as root on both the app and mon servers. | ||
use anyhow::{anyhow, Result}; | ||
use rustix::process::geteuid; | ||
use serde::Serialize; | ||
use std::{ | ||
fs, | ||
process::{self, ExitCode}, | ||
}; | ||
use url::Host; | ||
|
||
const STATE_PATH: &str = "/etc/securedrop-noble-migration.json"; | ||
|
||
#[derive(Serialize)] | ||
struct State { | ||
ssh: bool, | ||
ufw: bool, | ||
free_space: bool, | ||
apt: bool, | ||
systemd: bool, | ||
} | ||
|
||
/// Parse the OS codename from /etc/os-release | ||
fn os_codename() -> Result<String> { | ||
let contents = fs::read_to_string("/etc/os-release")?; | ||
for line in contents.lines() { | ||
if line.starts_with("VERSION_CODENAME=") { | ||
let (_, codename) = line.split_once("=").unwrap(); | ||
return Ok(codename.trim().to_string()); | ||
} | ||
} | ||
|
||
Err(anyhow!( | ||
"Could not find VERSION_CODENAME in /etc/os-release" | ||
)) | ||
} | ||
|
||
/// Check that the UNIX "ssh" group has no members | ||
fn check_ssh_group() -> Result<bool> { | ||
// There are no clean bindings to getgrpname in rustix, | ||
// so jut shell out to getent to get group members | ||
let output = process::Command::new("getent") | ||
.arg("group") | ||
.arg("ssh") | ||
.output()?; | ||
if output.status.code() == Some(2) { | ||
println!("ssh: group does not exist"); | ||
return Ok(true); | ||
} else if !output.status.success() { | ||
return Err(anyhow!("running getent failed",)); | ||
} | ||
|
||
let stdout = String::from_utf8(output.stdout)?; | ||
// The format looks like `ssh:x:123:member1,member2` | ||
let (_, members) = stdout.rsplit_once(':').unwrap(); | ||
if members.is_empty() { | ||
println!("ssh: group is empty"); | ||
Ok(true) | ||
} else { | ||
println!("ssh: group is not empty: {members}"); | ||
Ok(false) | ||
} | ||
} | ||
|
||
/// Check that ufw is removed | ||
fn check_ufw_removed() -> Result<bool> { | ||
if fs::exists("/usr/sbin/ufw")? { | ||
println!("ufw: ufw is still installed"); | ||
Ok(false) | ||
} else { | ||
println!("ufw: ufw was removed"); | ||
Ok(true) | ||
} | ||
} | ||
|
||
/// Check that there is enough free space | ||
fn check_free_space() -> Result<bool> { | ||
// Also no simple bindings to get disk size, so shell out to df | ||
let output = process::Command::new("df").arg("/").output()?; | ||
if !output.status.success() { | ||
return Err(anyhow!("running df failed",)); | ||
} | ||
|
||
let stdout = String::from_utf8(output.stdout)?; | ||
let (_, line) = stdout.split_once('\n').unwrap(); | ||
let parts: Vec<_> = line.split_whitespace().collect(); | ||
|
||
let free_space = parts[3].parse::<u64>()?; | ||
// Should be at least 10GB free | ||
if free_space < 10 * 1024 * 1024 * 1024 { | ||
println!("free space: not enough free space"); | ||
Ok(false) | ||
} else { | ||
println!("free space: enough free space"); | ||
Ok(true) | ||
} | ||
} | ||
|
||
const EXPECTED_DOMAINS: [&str; 4] = [ | ||
"archive.ubuntu.com", | ||
"security.ubuntu.com", | ||
"apt.freedom.press", | ||
"apt-test.freedom.press", | ||
]; | ||
|
||
/// Verify only expected sources are configured | ||
fn check_apt() -> Result<bool> { | ||
let output = process::Command::new("apt-get") | ||
.arg("indextargets") | ||
.output()?; | ||
if !output.status.success() { | ||
return Err(anyhow!("running apt-get indextargets failed",)); | ||
} | ||
|
||
let stdout = String::from_utf8(output.stdout)?; | ||
for line in stdout.lines() { | ||
if line.starts_with("URI:") { | ||
let uri = line.strip_prefix("URI: ").unwrap(); | ||
let parsed = url::Url::parse(uri)?; | ||
if let Some(Host::Domain(domain)) = parsed.host() { | ||
if !EXPECTED_DOMAINS.contains(&domain) { | ||
println!("apt: unexpected source: {domain}"); | ||
return Ok(false); | ||
} | ||
} else { | ||
println!("apt: unexpected source: {uri}"); | ||
return Ok(false); | ||
} | ||
} | ||
} | ||
|
||
println!("apt: all sources are expected"); | ||
Ok(true) | ||
} | ||
|
||
fn check_systemd() -> Result<bool> { | ||
let output = process::Command::new("systemctl") | ||
.arg("is-failed") | ||
.output()?; | ||
if output.status.success() { | ||
// success means some units are failed | ||
println!("systemd: some units are failed"); | ||
Ok(false) | ||
} else { | ||
println!("systemd: no failed units"); | ||
Ok(true) | ||
} | ||
} | ||
|
||
fn run() -> Result<ExitCode> { | ||
let codename = os_codename()?; | ||
if codename != "focal" { | ||
println!("Unsupported Ubuntu version: {codename}"); | ||
// nothing to do, write an empty JSON blob | ||
fs::write(STATE_PATH, "{}")?; | ||
return Ok(ExitCode::SUCCESS); | ||
} | ||
|
||
let state = State { | ||
ssh: check_ssh_group()?, | ||
ufw: check_ufw_removed()?, | ||
free_space: check_free_space()?, | ||
apt: check_apt()?, | ||
systemd: check_systemd()?, | ||
}; | ||
|
||
fs::write(STATE_PATH, serde_json::to_string(&state)?)?; | ||
if state.ssh && state.ufw && state.free_space && state.apt && state.systemd | ||
{ | ||
println!("All ready for migration!"); | ||
Ok(ExitCode::SUCCESS) | ||
} else { | ||
println!( | ||
"Some errors were found that will block migration. | ||
If you are unsure what to do, please contact the SecureDrop | ||
support team: <https://docs.securedrop.org/en/stable/getting_support.html>." | ||
); | ||
Ok(ExitCode::FAILURE) | ||
} | ||
} | ||
|
||
fn main() -> Result<ExitCode> { | ||
if !geteuid().is_root() { | ||
println!("This script must be run as root"); | ||
return Ok(ExitCode::FAILURE); | ||
} | ||
|
||
match run() { | ||
Ok(code) => Ok(code), | ||
Err(e) => { | ||
// Try to log the error in the least complex way possible | ||
fs::write(STATE_PATH, "{\"error\": true}")?; | ||
eprintln!("Error: {e}"); | ||
Ok(ExitCode::FAILURE) | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
7 changes: 7 additions & 0 deletions
7
securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.service
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[Unit] | ||
Description=Check noble migration readiness | ||
|
||
[Service] | ||
Type=oneshot | ||
ExecStart=/usr/bin/securedrop-noble-migration-check | ||
User=root |
10 changes: 10 additions & 0 deletions
10
securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.timer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[Unit] | ||
Description=Check noble migration readiness | ||
|
||
[Timer] | ||
OnCalendar=hourly | ||
Persistent=true | ||
RandomizedDelaySec=5m | ||
|
||
[Install] | ||
WantedBy=timers.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.