Watch
0
0
Fork
You've already forked hyperhive
0

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:
atlas 2026-10-09 15:53:39 +02:00 • committed by mara
commit 01b8387ab8
7 changed files with 156 additions and 16 deletions

View file

@ -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")]);
}
}