pub fn expand_file_env(name: &str) -> std::io::Result<()> {
let file_var = format!("{name}_FILE");
let path = match std::env::var(&file_var) {
Ok(p) if !p.trim().is_empty() => p,
_ => return Ok(()),
};
if let Ok(existing) = std::env::var(name) {
if !existing.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"both {name} and {file_var} are set — pick one (FILE form is for container/k8s secret mounts)"
),
));
}
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let Ok(meta) = std::fs::metadata(&path) {
let mode = meta.mode();
if mode & 0o077 != 0 {
tracing::warn!(
target: "reddb::secrets",
env = %file_var,
path = %path,
mode = format_args!("{:o}", mode & 0o7777),
"secret file is group/world-readable; consider chmod 0600"
);
}
}
}
let contents = std::fs::read_to_string(&path)?;
let value = contents.trim_end_matches(['\n', '\r']).to_string();
unsafe {
std::env::set_var(name, &value);
std::env::remove_var(&file_var);
}
tracing::info!(
target: "reddb::secrets",
env = %name,
path = %path,
"expanded {file_var} into {name}"
);
Ok(())
}
pub fn expand_all_reddb_secrets() -> Vec<(String, std::io::Error)> {
const VARS: &[&str] = &[
"REDDB_CERTIFICATE",
"REDDB_VAULT_KEY",
"REDDB_USERNAME",
"REDDB_PASSWORD",
"REDDB_ROOT_TOKEN",
];
let mut errors = Vec::new();
for var in VARS {
if let Err(err) = expand_file_env(var) {
errors.push((var.to_string(), err));
}
}
errors
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn env_lock() -> &'static Mutex<()> {
static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn tmpdir(label: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"reddb-secret-file-{}-{}-{}",
label,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn cleanup(name: &str) {
unsafe {
std::env::remove_var(name);
std::env::remove_var(format!("{name}_FILE"));
}
}
#[test]
fn no_op_when_neither_set() {
let _g = env_lock().lock();
cleanup("REDDB_TEST_NOOP");
assert!(expand_file_env("REDDB_TEST_NOOP").is_ok());
assert!(std::env::var("REDDB_TEST_NOOP").is_err());
}
#[test]
fn no_op_when_only_inline_set() {
let _g = env_lock().lock();
cleanup("REDDB_TEST_INLINE_ONLY");
unsafe {
std::env::set_var("REDDB_TEST_INLINE_ONLY", "inline-value");
}
assert!(expand_file_env("REDDB_TEST_INLINE_ONLY").is_ok());
assert_eq!(
std::env::var("REDDB_TEST_INLINE_ONLY").unwrap(),
"inline-value"
);
cleanup("REDDB_TEST_INLINE_ONLY");
}
#[test]
fn reads_file_and_strips_trailing_newline() {
let _g = env_lock().lock();
let dir = tmpdir("read");
let path = dir.join("secret");
std::fs::write(&path, "supersecret\n").unwrap();
cleanup("REDDB_TEST_READ");
unsafe {
std::env::set_var("REDDB_TEST_READ_FILE", &path);
}
expand_file_env("REDDB_TEST_READ").unwrap();
assert_eq!(std::env::var("REDDB_TEST_READ").unwrap(), "supersecret");
assert!(std::env::var("REDDB_TEST_READ_FILE").is_err());
cleanup("REDDB_TEST_READ");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn strips_crlf_endings() {
let _g = env_lock().lock();
let dir = tmpdir("crlf");
let path = dir.join("secret");
std::fs::write(&path, "windows-secret\r\n").unwrap();
cleanup("REDDB_TEST_CRLF");
unsafe {
std::env::set_var("REDDB_TEST_CRLF_FILE", &path);
}
expand_file_env("REDDB_TEST_CRLF").unwrap();
assert_eq!(std::env::var("REDDB_TEST_CRLF").unwrap(), "windows-secret");
cleanup("REDDB_TEST_CRLF");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn preserves_unicode() {
let _g = env_lock().lock();
let dir = tmpdir("unicode");
let path = dir.join("secret");
std::fs::write(&path, "p4ss-✓-π\n").unwrap();
cleanup("REDDB_TEST_UNI");
unsafe {
std::env::set_var("REDDB_TEST_UNI_FILE", &path);
}
expand_file_env("REDDB_TEST_UNI").unwrap();
assert_eq!(std::env::var("REDDB_TEST_UNI").unwrap(), "p4ss-✓-π");
cleanup("REDDB_TEST_UNI");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn errors_when_both_inline_and_file_set() {
let _g = env_lock().lock();
let dir = tmpdir("conflict");
let path = dir.join("secret");
std::fs::write(&path, "from-file").unwrap();
cleanup("REDDB_TEST_CONFLICT");
unsafe {
std::env::set_var("REDDB_TEST_CONFLICT", "from-env");
std::env::set_var("REDDB_TEST_CONFLICT_FILE", &path);
}
let err = expand_file_env("REDDB_TEST_CONFLICT").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(std::env::var("REDDB_TEST_CONFLICT").unwrap(), "from-env");
cleanup("REDDB_TEST_CONFLICT");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn missing_file_returns_not_found() {
let _g = env_lock().lock();
cleanup("REDDB_TEST_MISSING");
unsafe {
std::env::set_var("REDDB_TEST_MISSING_FILE", "/nonexistent/zzz/reddb-no-file");
}
let err = expand_file_env("REDDB_TEST_MISSING").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
cleanup("REDDB_TEST_MISSING");
}
#[test]
fn empty_file_path_var_is_no_op() {
let _g = env_lock().lock();
cleanup("REDDB_TEST_EMPTYP");
unsafe {
std::env::set_var("REDDB_TEST_EMPTYP_FILE", " ");
}
assert!(expand_file_env("REDDB_TEST_EMPTYP").is_ok());
assert!(std::env::var("REDDB_TEST_EMPTYP").is_err());
cleanup("REDDB_TEST_EMPTYP");
}
}