swarmctl bao: recreate the grant token file 0600, deliver init keys to stderr when stdout fails
This commit is contained in:
parent
a20596fa59
commit
52ba0a996f
1 changed files with 109 additions and 18 deletions
|
|
@ -15,6 +15,7 @@
|
||||||
//! message. The child's environment is readable only by root through
|
//! message. The child's environment is readable only by root through
|
||||||
//! `/proc/<pid>/environ`.
|
//! `/proc/<pid>/environ`.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
use std::io::{self, IsTerminal as _, Write as _};
|
use std::io::{self, IsTerminal as _, Write as _};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{Command, Output, Stdio};
|
use std::process::{Command, Output, Stdio};
|
||||||
|
|
@ -124,7 +125,7 @@ pub fn init(args: InitArgs) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_with(tools: &Tools, grant: Option<&GrantPaths>, out: &mut impl io::Write) -> Result<()> {
|
fn init_with(tools: &Tools, grant: Option<&GrantPaths>, out: &mut impl io::Write) -> Result<()> {
|
||||||
let root_token = initialise(tools, out)?;
|
let root_token = initialise(tools, out, &mut io::stderr().lock())?;
|
||||||
let Some(paths) = grant else {
|
let Some(paths) = grant else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"The store is initialised. Next, `swarmctl bao grant-bootstrap` bootstraps the \
|
"The store is initialised. Next, `swarmctl bao grant-bootstrap` bootstraps the \
|
||||||
|
|
@ -177,7 +178,14 @@ impl Drop for InitOutput {
|
||||||
|
|
||||||
/// Initialises the store, writes the shares and the root token to `out`, and
|
/// Initialises the store, writes the shares and the root token to `out`, and
|
||||||
/// returns the root token for the caller to use once and drop.
|
/// returns the root token for the caller to use once and drop.
|
||||||
fn initialise(tools: &Tools, out: &mut impl io::Write) -> Result<Zeroizing<String>> {
|
///
|
||||||
|
/// When `out` fails the store is initialised already and the secrets exist
|
||||||
|
/// nowhere else, so they are written to `fallback` once before failing.
|
||||||
|
fn initialise(
|
||||||
|
tools: &Tools,
|
||||||
|
out: &mut impl io::Write,
|
||||||
|
fallback: &mut impl io::Write,
|
||||||
|
) -> Result<Zeroizing<String>> {
|
||||||
let status = run(&mut tools.bao_cmd(&["operator", "init", "-status"]))?;
|
let status = run(&mut tools.bao_cmd(&["operator", "init", "-status"]))?;
|
||||||
match status.status.code() {
|
match status.status.code() {
|
||||||
Some(0) => bail!(
|
Some(0) => bail!(
|
||||||
|
|
@ -212,20 +220,36 @@ fn initialise(tools: &Tools, out: &mut impl io::Write) -> Result<Zeroizing<Strin
|
||||||
"Record the keys and the root token below off this host now: swarmctl keeps none of \
|
"Record the keys and the root token below off this host now: swarmctl keeps none of \
|
||||||
them and cannot show them again."
|
them and cannot show them again."
|
||||||
);
|
);
|
||||||
let printed = (|| -> io::Result<()> {
|
if let Err(stdout_err) = write_secrets(out, &parsed) {
|
||||||
for (i, key) in parsed.unseal_keys_b64.iter().enumerate() {
|
let delivered = write_secrets(fallback, &parsed);
|
||||||
writeln!(out, "Unseal key {}: {key}", i + 1)?;
|
return Err(match delivered {
|
||||||
}
|
Ok(()) => anyhow::anyhow!(
|
||||||
for (i, key) in parsed.recovery_keys_b64.iter().enumerate() {
|
"the store is now initialised, but its keys and root token could not be written \
|
||||||
writeln!(out, "Recovery key {}: {key}", i + 1)?;
|
to stdout ({stdout_err}); they were written to stderr instead, once: record \
|
||||||
}
|
them now"
|
||||||
writeln!(out, "Initial root token: {}", parsed.root_token)?;
|
),
|
||||||
out.flush()
|
Err(stderr_err) => anyhow::anyhow!(
|
||||||
})();
|
"the store is now initialised, but its keys and root token could not be \
|
||||||
printed.context("writing the keys and root token to stdout")?;
|
written to stdout ({stdout_err}) or to stderr ({stderr_err}) and are lost; \
|
||||||
|
see OpenBao's `operator rekey` and `operator generate-root` documentation \
|
||||||
|
for what a remaining key holder can recover"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
Ok(Zeroizing::new(parsed.root_token.clone()))
|
Ok(Zeroizing::new(parsed.root_token.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_secrets(out: &mut impl io::Write, parsed: &InitOutput) -> io::Result<()> {
|
||||||
|
for (i, key) in parsed.unseal_keys_b64.iter().enumerate() {
|
||||||
|
writeln!(out, "Unseal key {}: {key}", i + 1)?;
|
||||||
|
}
|
||||||
|
for (i, key) in parsed.recovery_keys_b64.iter().enumerate() {
|
||||||
|
writeln!(out, "Recovery key {}: {key}", i + 1)?;
|
||||||
|
}
|
||||||
|
writeln!(out, "Initial root token: {}", parsed.root_token)?;
|
||||||
|
out.flush()
|
||||||
|
}
|
||||||
|
|
||||||
/// Polls until `bao status` reports an unsealed store (exit 0).
|
/// Polls until `bao status` reports an unsealed store (exit 0).
|
||||||
fn wait_unsealed(tools: &Tools) -> Result<()> {
|
fn wait_unsealed(tools: &Tools) -> Result<()> {
|
||||||
let deadline = Instant::now() + tools.unseal_wait;
|
let deadline = Instant::now() + tools.unseal_wait;
|
||||||
|
|
@ -306,6 +330,16 @@ fn grant_bootstrap_with(tools: &Tools, paths: &GrantPaths, root: &str) -> Result
|
||||||
|
|
||||||
let secrets = [root, grant_token.as_str()];
|
let secrets = [root, grant_token.as_str()];
|
||||||
let contents = Zeroizing::new(format!("{}\n", *grant_token));
|
let contents = Zeroizing::new(format!("{}\n", *grant_token));
|
||||||
|
// `write_atomic` keeps an existing file's mode and owner; this one must be
|
||||||
|
// ours and 0600 whatever was there.
|
||||||
|
match fs::remove_file(&paths.token_file) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e)
|
||||||
|
.with_context(|| format!("removing the old {}", paths.token_file.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
crate::write_atomic(&paths.token_file, &contents).with_context(|| {
|
crate::write_atomic(&paths.token_file, &contents).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"placing the bootstrap token at {}",
|
"placing the bootstrap token at {}",
|
||||||
|
|
@ -503,7 +537,7 @@ exit 9
|
||||||
fn init_prints_the_keys_and_the_root_token_once() {
|
fn init_prints_the_keys_and_the_root_token_once() {
|
||||||
let f = Fixture::new("init-once");
|
let f = Fixture::new("init-once");
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let token = initialise(&f.tools, &mut out).expect("init succeeds");
|
let token = initialise(&f.tools, &mut out, &mut Vec::new()).expect("init succeeds");
|
||||||
let out = String::from_utf8(out).expect("utf8");
|
let out = String::from_utf8(out).expect("utf8");
|
||||||
|
|
||||||
assert_eq!(&*token, ROOT);
|
assert_eq!(&*token, ROOT);
|
||||||
|
|
@ -529,7 +563,7 @@ exit 9
|
||||||
#[test]
|
#[test]
|
||||||
fn init_hands_bao_no_token_in_argv_or_environment() {
|
fn init_hands_bao_no_token_in_argv_or_environment() {
|
||||||
let f = Fixture::new("init-argv");
|
let f = Fixture::new("init-argv");
|
||||||
initialise(&f.tools, &mut Vec::new()).expect("init succeeds");
|
initialise(&f.tools, &mut Vec::new(), &mut Vec::new()).expect("init succeeds");
|
||||||
let log = f.log();
|
let log = f.log();
|
||||||
assert!(!log.contains(ROOT), "{log}");
|
assert!(!log.contains(ROOT), "{log}");
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -543,7 +577,7 @@ exit 9
|
||||||
let f = Fixture::new("init-refuse");
|
let f = Fixture::new("init-refuse");
|
||||||
f.set("init_status", "0");
|
f.set("init_status", "0");
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let err = initialise(&f.tools, &mut out).expect_err("must refuse");
|
let err = initialise(&f.tools, &mut out, &mut Vec::new()).expect_err("must refuse");
|
||||||
assert!(
|
assert!(
|
||||||
format!("{err:#}").contains("already initialised"),
|
format!("{err:#}").contains("already initialised"),
|
||||||
"{err:#}"
|
"{err:#}"
|
||||||
|
|
@ -560,7 +594,7 @@ exit 9
|
||||||
fn init_reports_an_unreadable_status_instead_of_initialising() {
|
fn init_reports_an_unreadable_status_instead_of_initialising() {
|
||||||
let f = Fixture::new("init-status-err");
|
let f = Fixture::new("init-status-err");
|
||||||
f.set("init_status", "1");
|
f.set("init_status", "1");
|
||||||
let err = initialise(&f.tools, &mut Vec::new()).expect_err("must stop");
|
let err = initialise(&f.tools, &mut Vec::new(), &mut Vec::new()).expect_err("must stop");
|
||||||
assert!(format!("{err:#}").contains("init status"), "{err:#}");
|
assert!(format!("{err:#}").contains("init status"), "{err:#}");
|
||||||
assert!(!f.log().contains("-format=json"), "{}", f.log());
|
assert!(!f.log().contains("-format=json"), "{}", f.log());
|
||||||
}
|
}
|
||||||
|
|
@ -579,7 +613,7 @@ exit 9
|
||||||
.expect("rejects");
|
.expect("rejects");
|
||||||
assert!(parser.to_string().contains("hvs.LEAKED"), "{parser}");
|
assert!(parser.to_string().contains("hvs.LEAKED"), "{parser}");
|
||||||
|
|
||||||
let err = initialise(&f.tools, &mut Vec::new()).expect_err("must fail");
|
let err = initialise(&f.tools, &mut Vec::new(), &mut Vec::new()).expect_err("must fail");
|
||||||
assert!(!format!("{err:#}").contains("hvs.LEAKED"), "{err:#}");
|
assert!(!format!("{err:#}").contains("hvs.LEAKED"), "{err:#}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -621,6 +655,63 @@ exit 9
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A writer that refuses every write, as a closed pipe or full disk does.
|
||||||
|
struct Broken;
|
||||||
|
|
||||||
|
impl io::Write for Broken {
|
||||||
|
fn write(&mut self, _: &[u8]) -> io::Result<usize> {
|
||||||
|
Err(io::Error::other("broken pipe"))
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Err(io::Error::other("broken pipe"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_failing_stdout_falls_back_to_stderr_and_says_the_store_is_initialised() {
|
||||||
|
let f = Fixture::new("stdout-fails");
|
||||||
|
let mut fallback = Vec::new();
|
||||||
|
let err = initialise(&f.tools, &mut Broken, &mut fallback).expect_err("must fail");
|
||||||
|
let text = format!("{err:#}");
|
||||||
|
assert!(text.contains("now initialised"), "{text}");
|
||||||
|
assert!(text.contains("written to stderr"), "{text}");
|
||||||
|
assert!(!text.contains(ROOT) && !text.contains("UK1"), "{text}");
|
||||||
|
let fallback = String::from_utf8(fallback).expect("utf8");
|
||||||
|
assert_eq!(count(&fallback, ROOT), 1, "{fallback}");
|
||||||
|
assert_eq!(count(&fallback, "Unseal key 1: UK1"), 1, "{fallback}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keys_that_neither_stdout_nor_stderr_accept_are_reported_lost() {
|
||||||
|
let f = Fixture::new("both-fail");
|
||||||
|
let err = initialise(&f.tools, &mut Broken, &mut Broken).expect_err("must fail");
|
||||||
|
let text = format!("{err:#}");
|
||||||
|
assert!(text.contains("now initialised"), "{text}");
|
||||||
|
assert!(text.contains("are lost"), "{text}");
|
||||||
|
assert!(text.contains("rekey"), "{text}");
|
||||||
|
assert!(!text.contains(ROOT), "{text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_existing_world_readable_token_file_is_replaced_by_a_0600_one() {
|
||||||
|
let f = Fixture::new("token-mode");
|
||||||
|
fs::write(&f.paths.token_file, "old\n").expect("seed");
|
||||||
|
fs::set_permissions(&f.paths.token_file, fs::Permissions::from_mode(0o644)).expect("chmod");
|
||||||
|
|
||||||
|
grant_bootstrap_with(&f.tools, &f.paths, ROOT).expect("grant succeeds");
|
||||||
|
|
||||||
|
let mode = fs::metadata(&f.paths.token_file)
|
||||||
|
.expect("stat")
|
||||||
|
.permissions()
|
||||||
|
.mode()
|
||||||
|
& 0o7777;
|
||||||
|
assert_eq!(mode, 0o600);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&f.paths.token_file).expect("read"),
|
||||||
|
format!("{GRANT}\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn grant_on_a_bootstrapped_store_changes_nothing() {
|
fn grant_on_a_bootstrapped_store_changes_nothing() {
|
||||||
let f = Fixture::new("grant-again");
|
let f = Fixture::new("grant-again");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue