Skip to content

Commit

Permalink
WIP: Use a single script to set and reset Redis password
Browse files Browse the repository at this point in the history
Code to set and reset Redis passwords has now proliferated into the
postinst, dev environment, ansible provisioning and backup restore.
Instead of maintaining separate code for this, write a single script
that handles it all.

A new `securedrop-set-redis-auth` tool offers three operations:
* check: verify all the passwords are in sync (for testinfra checks)
* reset: forcibly change the password everywhere (for backup restore)
* reset-if-needed: change the password everywhere if needed (for
postinst)

Fixes #7386.
  • Loading branch information
legoktm committed Dec 14, 2024
1 parent 0798935 commit b98381e
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 16 deletions.
102 changes: 89 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]

members = [
"misc-rust",
"noble-migration",
"redwood",
]
Expand Down
1 change: 1 addition & 0 deletions builder/build-debs-securedrop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ set -euxo pipefail
# Make a copy of the source tree since we do destructive operations on it
cp -R /src/securedrop /srv/securedrop
mkdir /srv/rust
cp -R /src/misc-rust /srv/rust/misc-rust
cp -R /src/noble-migration /srv/rust/noble-migration
cp -R /src/redwood /srv/rust/redwood
cp /src/Cargo.{toml,lock} /srv/rust/
Expand Down
10 changes: 10 additions & 0 deletions misc-rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "misc-rust"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.94"
base64 = "0.22.1"
rand = "0.8.5"
regex = "1.11.1"
156 changes: 156 additions & 0 deletions misc-rust/src/bin/set_redis_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use anyhow::{anyhow, Context, Ok, Result};
use base64::Engine;
use rand::RngCore;
use regex::Regex;
use std::{fs, process::ExitCode, sync::LazyLock};

const CONFIG_PY_PATH: &str = "/var/www/securedrop/config.py";
const RQ_CONFIG_PY_PATH: &str = "/var/www/securedrop/rq_config.py";
const REDIS_CONF_PATH: &str = "/etc/redis/redis.conf";

static PYTHON_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"^REDIS_PASSWORD = ["'](.*?)["']$"#).unwrap()
});

static REDIS_CONF_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^requirepass (.*?)$"#).unwrap());

/// Extract the password from our config.py or rq_config.py files
fn read_python_file(path: &str) -> Result<Option<String>> {
let contents =
fs::read_to_string(path).context(format!("reading {path} failed"))?;
// Read in reverse because we want to look for the last matching line
// since it'll take precedence in Python
for line in contents.lines().rev() {
if let Some(cap) = PYTHON_RE.captures(line) {
return Ok(Some(cap[1].to_string()));
}
}

// Nothing found
Ok(None)
}

/// Set the new password in a Python file, removing any existing passwords
fn write_python_file(path: &str, password: &str) -> Result<()> {
let contents =
fs::read_to_string(path).context(format!("reading {path} failed"))?;

// Take the existing file, remove any matching password lines, and then add
// our new password at the end
let contents = contents
.lines()
.filter(|line| !PYTHON_RE.is_match(line))
.chain([format!("REDIS_PASSWORD = '{password}'").as_str(), ""])
.collect::<Vec<_>>()
.join("\n");
fs::write(path, contents).context(format!("writing {path} failed"))?;
Ok(())
}

/// Extract the password from a redis.conf file
fn read_redis_conf(path: &str) -> Result<Option<String>> {
let contents =
fs::read_to_string(path).context(format!("reading {path} failed"))?;
// Read in reverse because we want to look for the last matching line
// since redis uses the last requirepass stanza
for line in contents.lines().rev() {
if let Some(cap) = REDIS_CONF_RE.captures(line) {
return Ok(Some(cap[1].to_string()));
}
}

// Nothing found
Ok(None)
}

/// Set the new password in a redis.conf file, removing any existing passwords
fn write_redis_conf(path: &str, password: &str) -> Result<()> {
let contents =
fs::read_to_string(path).context(format!("reading {path} failed"))?;
// Take the existing file, remove any matching password lines, and then add
// our new password at the end
let contents = contents
.lines()
.filter(|line| !REDIS_CONF_RE.is_match(line))
.chain([format!("requirepass {password}").as_str(), ""])
.collect::<Vec<_>>()
.join("\n");
fs::write(path, contents).context(format!("writing {path} failed"))?;
Ok(())
}

/// Generate a base64-encoded, 32-byte random string
///
/// This is roughly equivalent to `head -c 32 /dev/urandom | base64`
fn generate_password() -> String {
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
base64::engine::general_purpose::STANDARD.encode(bytes)
}

/// Check that all three files have a password set and that it's the same
fn check() -> Result<bool> {
let config_py = read_python_file(CONFIG_PY_PATH)?;
let rq_config_py = read_python_file(RQ_CONFIG_PY_PATH)?;
let redis_conf = read_redis_conf(REDIS_CONF_PATH)?;
Ok(config_py.is_some()
&& rq_config_py.is_some()
&& redis_conf.is_some()
&& config_py == rq_config_py
&& rq_config_py == redis_conf)
}

/// Reset the passwords in all three files
fn reset() -> Result<()> {
let password = generate_password();
write_python_file(CONFIG_PY_PATH, &password)?;
write_python_file(RQ_CONFIG_PY_PATH, &password)?;
write_redis_conf(REDIS_CONF_PATH, &password)?;
// TODO: Should we restart redis/apache2 here?
Ok(())
}

fn main() -> Result<ExitCode> {
let mode = std::env::args()
.nth(1)
.ok_or_else(|| anyhow!("missing mode"))?;

match mode.as_str() {
"check" => {
if check()? {
println!("Yay, all three passwords are the same!");
Ok(ExitCode::SUCCESS)
} else {
println!("Error: Passwords are not all the same!");
Ok(ExitCode::FAILURE)
}
}
"reset" => {
reset()?;
println!("All three passwords have been changed");
Ok(ExitCode::SUCCESS)
}
"reset-if-needed" => {
if !check()? {
reset()?;
println!("All three passwords have been changed");
} else {
println!("All three passwords are the same; nothing changed");
}
Ok(ExitCode::SUCCESS)
}
unknown => Err(anyhow!("unknown mode: {unknown}")),
}
}

#[test]
fn test_read_python_file() {
assert_eq!(
read_python_file("../securedrop/config.py.example")
.unwrap()
.unwrap(),
"{{ redis_password.stdout }}"
);
}
5 changes: 5 additions & 0 deletions securedrop/debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ override_dh_auto_install:
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
# Build securedrop-app-code Rust code
cd /srv/rust/misc-rust && cargo build --release --locked && \
cd /srv/securedrop && \
mkdir -p ./debian/securedrop-app-code/usr/bin && \
mv /srv/rust/target/release/set_redis_auth ./debian/securedrop-config/usr/bin/securedrop-set-redis-auth
# 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
Expand Down
3 changes: 3 additions & 0 deletions supply-chain/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ notes = "Haiku OS-only"
criteria = []
notes = "WASM-only"

[policy.misc-rust]
criteria = "safe-to-run"

[policy.noble-migration]
criteria = "safe-to-run"

Expand Down
Loading

0 comments on commit b98381e

Please sign in to comment.