-
Notifications
You must be signed in to change notification settings - Fork 696
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Use a single script to set and reset Redis password
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
Showing
8 changed files
with
268 additions
and
16 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
[workspace] | ||
|
||
members = [ | ||
"misc-rust", | ||
"noble-migration", | ||
"redwood", | ||
] | ||
|
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,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" |
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,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 }}" | ||
); | ||
} |
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
Oops, something went wrong.