write secret and key files atomically with their final mode
Each of these wrote its file in place (or set the mode after the write), so a write cut short by a kill or a full disk left a partial file behind, and some secrets existed for a moment at umask mode: - swarm-authelia/swarm-grafana/hive-forge/swarm-bao generators: a partial file passed the `-s`/`-e` guard and was kept forever. They now capture the value first and land it with `atomic_write_secret`. - swarm-controller webhook secret: a truncated file failed the 64-hex check and the secret was regenerated, out of step with the forge hooks. It is now written 0600 to a sibling temp, fsynced, renamed. - hive-c0re htpasswd: the temp is now fsynced and set 0644 before the rename, like the sibling agents.conf writer, instead of relying on c0re's umask. - hivectl wg init: the private key is created 0400 in a temp and renamed, instead of written at umask mode and chmod'ed after. Closes #5068
This commit is contained in:
parent
1726ad3bad
commit
01b8387ab8
7 changed files with 156 additions and 16 deletions
|
|
@ -94,10 +94,7 @@ pub(crate) async fn wg_init(socket: &Path, address: Option<&str>) -> Result<()>
|
|||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
std::fs::write(key_path, &out.stdout)
|
||||
.with_context(|| format!("write {}", key_path.display()))?;
|
||||
std::fs::set_permissions(key_path, std::fs::Permissions::from_mode(0o400))
|
||||
.with_context(|| format!("chmod 400 {}", key_path.display()))?;
|
||||
write_key_0400(key_path, &out.stdout)?;
|
||||
println!("Generated WireGuard private key at {WG_KEY_PATH} (0400).");
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +122,33 @@ pub(crate) async fn wg_init(socket: &Path, address: Option<&str>) -> Result<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Write `key` to a sibling temp file created `0400` and rename it over
|
||||
/// `path`, so the key is never readable by anyone else and a crash never
|
||||
/// leaves half of it at `path`. Errors carry paths, never the key.
|
||||
fn write_key_0400(path: &Path, key: &[u8]) -> Result<()> {
|
||||
use std::io::Write as _;
|
||||
use std::os::unix::fs::OpenOptionsExt as _;
|
||||
|
||||
let mut tmp_name = path
|
||||
.file_name()
|
||||
.with_context(|| format!("{} has no file name", path.display()))?
|
||||
.to_owned();
|
||||
tmp_name.push(".tmp");
|
||||
let tmp = path.with_file_name(tmp_name);
|
||||
// A temp file left by a crashed earlier write would fail `create_new`.
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(0o400)
|
||||
.open(&tmp)
|
||||
.with_context(|| format!("create {}", tmp.display()))?;
|
||||
f.write_all(key)
|
||||
.and_then(|()| f.sync_all())
|
||||
.with_context(|| format!("write {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, path).with_context(|| format!("rename onto {}", path.display()))
|
||||
}
|
||||
|
||||
/// Derive a WireGuard public key from a private key by piping it through
|
||||
/// `wg pubkey`.
|
||||
fn wg_pubkey(privkey: &[u8]) -> Result<String> {
|
||||
|
|
@ -252,7 +276,7 @@ pub(crate) fn wg_status() -> Result<()> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{STORE_PKI_ROOT_PATH, store_root_step};
|
||||
use super::{STORE_PKI_ROOT_PATH, store_root_step, write_key_0400};
|
||||
|
||||
const PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----";
|
||||
|
||||
|
|
@ -275,4 +299,29 @@ mod tests {
|
|||
assert!(!s.contains("BEGIN CERTIFICATE"), "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The key lands at `0400` with its bytes intact, replacing whatever was
|
||||
/// there, and no temp file is left beside it.
|
||||
#[test]
|
||||
fn key_is_written_0400_with_no_temp_left() {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("hivectl-wg-key-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).expect("mkdir");
|
||||
let path = dir.join("hive.key");
|
||||
std::fs::write(&path, "stale").expect("seed");
|
||||
|
||||
write_key_0400(&path, b"key-bytes\n").expect("write");
|
||||
let mode = std::fs::metadata(&path).expect("stat").permissions().mode() & 0o777;
|
||||
let content = std::fs::read(&path).expect("read");
|
||||
let entries: Vec<_> = std::fs::read_dir(&dir)
|
||||
.expect("readdir")
|
||||
.map(|e| e.expect("entry").file_name())
|
||||
.collect();
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
|
||||
assert_eq!(mode, 0o400);
|
||||
assert_eq!(content, b"key-bytes\n");
|
||||
assert_eq!(entries, [std::ffi::OsString::from("hive.key")]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue