bxcore, gatewayd, loopd: the secret store moves to bxcore::secrets

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-27 19:08:42 -07:00
co-authored by ornith-1.5-35b-a3b
parent de60c9e13a
commit f594e0d9d4
20 changed files with 385 additions and 146 deletions
+285
View File
@@ -0,0 +1,285 @@
//! The secret store (M4a spec, section 4), moved from `gatewayd` to `bxcore` in M5b (spec, section
//! 5) with its behaviour unchanged. The environment is passed in as a function, so no test changes
//! the process's environment. Run with `--features secrets`. Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use std::collections::HashMap;
use std::ffi::OsString;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use bxcore::secrets::{RUNBOOK, RUNBOOK_FILE, SecretSource, SecretSpec, load};
use tmp::TempDir;
const TOKEN: &str = "s3cret-t0ken-value";
fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<OsString> + use<> {
let map: HashMap<String, OsString> = pairs
.iter()
.map(|(k, v)| (k.to_string(), OsString::from(v)))
.collect();
move |k| map.get(k).cloned()
}
fn owner_file(dir: &TempDir, name: &str, text: &str, mode: u32) -> PathBuf {
let path = dir.write(name, text);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).unwrap();
path
}
fn refused(source: &SecretSource, env: &dyn Fn(&str) -> Option<OsString>, word: &str) {
let e = load("gatewayd", "mattermost_token", source, env).expect_err(word);
let text = e.to_string();
assert!(text.contains(word), "{word}: {text}");
assert!(text.starts_with("secret mattermost_token: "), "{text}");
assert!(text.ends_with(RUNBOOK), "{text}");
assert!(
!text.contains(TOKEN),
"a refusal never shows the value: {text}"
);
}
#[test]
fn a_systemd_credential() {
let dir = TempDir::new("cred");
dir.write("creds/mattermost-token", &format!("{TOKEN}\n"));
let creds = dir.path().join("creds");
let env = env_of(&[("CREDENTIALS_DIRECTORY", creds.to_str().unwrap())]);
let got = load(
"gatewayd",
"mattermost_token",
&SecretSource::Credential("mattermost-token".into()),
&env,
)
.unwrap();
assert_eq!(
got.secret.expose(),
TOKEN,
"one trailing newline is removed"
);
assert_eq!(got.warning, None);
}
#[test]
fn a_credential_outside_systemd_or_missing_is_refused() {
let src = SecretSource::Credential("mattermost-token".into());
refused(&src, &env_of(&[]), "CREDENTIALS_DIRECTORY is not set");
let dir = TempDir::new("cred-missing");
refused(
&src,
&env_of(&[("CREDENTIALS_DIRECTORY", dir.path().to_str().unwrap())]),
"cannot read the credential",
);
}
#[test]
fn an_environment_variable() {
let got = load(
"gatewayd",
"mattermost_token",
&SecretSource::Env("MM".into()),
&env_of(&[("MM", TOKEN)]),
)
.unwrap();
assert_eq!(got.secret.expose(), TOKEN);
assert_eq!(got.warning, None, "only a file warns");
refused(&SecretSource::Env("MM".into()), &env_of(&[]), "is not set");
refused(
&SecretSource::Env("MM".into()),
&env_of(&[("MM", "")]),
"empty",
);
}
#[test]
fn an_owner_only_file_is_read_with_a_warning() {
let dir = TempDir::new("file");
for mode in [0o600, 0o400] {
let path = owner_file(
&dir,
&format!("token-{mode:o}"),
&format!("{TOKEN}\n"),
mode,
);
let got = load(
"gatewayd",
"mattermost_token",
&SecretSource::File(path.clone()),
&env_of(&[]),
)
.unwrap();
assert_eq!(got.secret.expose(), TOKEN);
let warning = got.warning.expect("a file secret warns");
assert!(
warning.starts_with(
"gatewayd: warning: secret mattermost_token is read in plaintext from "
),
"{warning}"
);
assert!(warning.contains(path.to_str().unwrap()), "{warning}");
assert!(warning.contains(RUNBOOK_FILE), "{warning}");
assert!(!warning.contains(TOKEN));
}
}
#[test]
fn a_file_anyone_else_can_read_or_that_is_not_a_plain_file_is_refused() {
let dir = TempDir::new("file-bad");
for (mode, _) in [
(0o640, "group"),
(0o604, "other"),
(0o644, "both"),
(0o660, "group write"),
] {
let path = owner_file(&dir, &format!("t-{mode:o}"), TOKEN, mode);
refused(
&SecretSource::File(path),
&env_of(&[]),
"only the owner may read it",
);
}
let target = owner_file(&dir, "real", TOKEN, 0o600);
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
refused(&SecretSource::File(link), &env_of(&[]), "symbolic link");
std::fs::create_dir(dir.path().join("adir")).unwrap();
refused(
&SecretSource::File(dir.path().join("adir")),
&env_of(&[]),
"not a regular file",
);
refused(
&SecretSource::File(dir.path().join("missing")),
&env_of(&[]),
"cannot read",
);
refused(
&SecretSource::File(PathBuf::from("relative/token")),
&env_of(&[]),
"absolute",
);
}
#[test]
fn a_file_another_user_owns_is_refused() {
// A regular file that root owns on Linux, NixOS and macOS; the tests never run as root.
refused(
&SecretSource::File(PathBuf::from("/etc/passwd")),
&env_of(&[]),
"/etc/passwd is not owned by the user gatewayd runs as",
);
}
#[test]
fn empty_and_non_utf8_values_are_refused() {
let dir = TempDir::new("value");
refused(
&SecretSource::File(owner_file(&dir, "empty", "", 0o600)),
&env_of(&[]),
"empty",
);
refused(
&SecretSource::File(owner_file(&dir, "nl", "\n", 0o600)),
&env_of(&[]),
"empty",
);
let path = dir.path().join("bin");
std::fs::write(&path, [0xff, 0xfe]).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
refused(&SecretSource::File(path), &env_of(&[]), "not UTF-8");
}
#[test]
fn only_one_trailing_newline_is_removed_and_spaces_stay() {
let dir = TempDir::new("trim");
let path = owner_file(&dir, "t", " a b \n\n", 0o600);
let got = load("gatewayd", "x", &SecretSource::File(path), &env_of(&[])).unwrap();
assert_eq!(got.secret.expose(), " a b \n");
}
#[test]
fn a_secret_prints_nothing_of_itself() {
let got = load(
"gatewayd",
"mattermost_token",
&SecretSource::Env("MM".into()),
&env_of(&[("MM", TOKEN)]),
)
.unwrap();
let shown = format!("{:?} {:?}", got.secret, got);
assert!(!shown.contains(TOKEN), "{shown}");
assert!(shown.contains("Secret(…)"), "{shown}");
}
#[test]
fn the_sentences_name_the_role_that_loads() {
let e = load(
"memoryd",
"honcho_key",
&SecretSource::Credential("honcho-key".into()),
&env_of(&[]),
)
.expect_err("no CREDENTIALS_DIRECTORY");
assert!(
e.to_string()
.contains("memoryd was not started by systemd with LoadCredentialEncrypted="),
"{e}"
);
let dir = TempDir::new("role");
let path = owner_file(&dir, "key", TOKEN, 0o600);
let got = load(
"memoryd",
"honcho_key",
&SecretSource::File(path),
&env_of(&[]),
)
.unwrap();
let warning = got.warning.expect("a file secret warns");
assert!(
warning.starts_with("memoryd: warning: secret honcho_key is read in plaintext from "),
"{warning}"
);
let e = load(
"memoryd",
"honcho_key",
&SecretSource::File(PathBuf::from("/etc/passwd")),
&env_of(&[]),
)
.expect_err("root owns it");
assert!(
e.to_string()
.contains("/etc/passwd is not owned by the user memoryd runs as"),
"{e}"
);
}
#[test]
fn a_spec_names_exactly_one_well_formed_source() {
let spec = |text: &str| toml::from_str::<SecretSpec>(text).unwrap().source();
assert_eq!(
spec("credential = \"mattermost-token\""),
Ok(SecretSource::Credential("mattermost-token".into()))
);
assert_eq!(spec("env = \"MM_2\""), Ok(SecretSource::Env("MM_2".into())));
assert_eq!(
spec("file = \"/run/token\""),
Ok(SecretSource::File(PathBuf::from("/run/token")))
);
for (text, word) in [
("", "needs exactly one of credential, env and file"),
("env = \"A\"\nfile = \"/t\"", "needs exactly one"),
("credential = \"\"", "is not a credential name"),
("credential = \"a/b\"", "is not a credential name"),
("env = \"mm\"", "is not a variable name"),
("file = \"token\"", "must be an absolute path"),
] {
let why = spec(text).expect_err(text);
assert!(why.contains(word), "{text:?}: {why}");
}
assert!(
toml::from_str::<SecretSpec>("env = \"A\"\npath = \"/t\"").is_err(),
"unknown keys are refused"
);
}
+92
View File
@@ -0,0 +1,92 @@
//! A secret file is read from the file that was checked, never from whatever the path names a
//! moment later (M4a review, finding 2). `read_checked` runs `between` after checking the path and
//! before opening it; each test swaps something there. Moved from `gatewayd` in M5b; run with
//! `--features secrets`. Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use bxcore::secrets::read_checked;
use tmp::TempDir;
const TOKEN: &str = "the-real-token";
const OTHER: &str = "a-file-the-owner-never-chose";
fn owner_file(dir: &TempDir, name: &str, text: &str) -> PathBuf {
let path = dir.write(name, text);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
path
}
fn refused(path: &Path, between: &dyn Fn(), word: &str) {
let why = read_checked("gatewayd", path, between).expect_err(word);
assert!(why.contains(word), "{word}: {why}");
assert!(why.contains(&path.display().to_string()), "{why}");
assert!(
!why.contains(TOKEN) && !why.contains(OTHER),
"never a value: {why}"
);
}
#[test]
fn an_untouched_file_is_read() {
let dir = TempDir::new("race-ok");
let path = owner_file(&dir, "token", &format!("{TOKEN}\n"));
let bytes = read_checked("gatewayd", &path, &|| {}).unwrap();
assert_eq!(bytes.as_slice(), format!("{TOKEN}\n").as_bytes());
}
#[test]
fn a_file_swapped_for_a_link_is_refused() {
let dir = TempDir::new("race-link");
let path = owner_file(&dir, "token", TOKEN);
let other = owner_file(&dir, "other", OTHER);
let swap = || {
std::fs::remove_file(&path).unwrap();
std::os::unix::fs::symlink(&other, &path).unwrap();
};
refused(&path, &swap, "changed while it was read");
}
#[test]
fn a_file_swapped_for_another_file_is_refused() {
let dir = TempDir::new("race-rename");
let path = owner_file(&dir, "token", TOKEN);
let other = owner_file(&dir, "other", OTHER);
let swap = || std::fs::rename(&other, &path).unwrap();
refused(&path, &swap, "changed while it was read");
}
#[test]
fn the_checks_hold_for_the_file_that_is_read() {
let dir = TempDir::new("race-mode");
let path = owner_file(&dir, "token", TOKEN);
let widen = || {
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
};
refused(&path, &widen, "has mode 644");
}
#[test]
fn the_path_checks_still_come_first() {
let dir = TempDir::new("race-first");
let other = owner_file(&dir, "other", OTHER);
let link = dir.path().join("link");
std::os::unix::fs::symlink(&other, &link).unwrap();
refused(
&link,
&|| panic!("never reached for a link"),
"is a symbolic link",
);
refused(
dir.path(),
&|| panic!("never reached for a directory"),
"is not a regular file",
);
let relative = Path::new("relative/token");
let why = read_checked("gatewayd", relative, &|| panic!("never reached")).unwrap_err();
assert!(why.contains("is not an absolute path"), "{why}");
}
+40
View File
@@ -0,0 +1,40 @@
//! Temporary directories for tests. Do not edit.
#![allow(dead_code)] // each test file uses a different part of this module
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
static NEXT: AtomicU32 = AtomicU32::new(0);
pub struct TempDir(PathBuf);
impl TempDir {
pub fn new(tag: &str) -> TempDir {
let n = NEXT.fetch_add(1, Ordering::SeqCst);
let path = std::env::temp_dir().join(format!("bx-{tag}-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).unwrap();
TempDir(path)
}
pub fn path(&self) -> &Path {
&self.0
}
/// Writes `text` to `name` inside the directory and returns the full path.
pub fn write(&self, name: &str, text: &str) -> PathBuf {
let path = self.0.join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&path, text).unwrap();
path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}