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
|
|
@ -317,7 +317,10 @@ fn htpasswd_read() -> Result<Vec<String>> {
|
|||
/// Write lines back atomically (`<path>.tmp` then rename), with a trailing
|
||||
/// newline.
|
||||
fn htpasswd_write(lines: &[String]) -> Result<()> {
|
||||
let path = htpasswd_path();
|
||||
htpasswd_write_at(&htpasswd_path(), lines)
|
||||
}
|
||||
|
||||
fn htpasswd_write_at(path: &std::path::Path, lines: &[String]) -> Result<()> {
|
||||
let tmp = path.with_extension("htpasswd.tmp");
|
||||
let content = if lines.is_empty() {
|
||||
String::new()
|
||||
|
|
@ -326,9 +329,16 @@ fn htpasswd_write(lines: &[String]) -> Result<()> {
|
|||
s.push('\n');
|
||||
s
|
||||
};
|
||||
std::fs::write(&tmp, &content)
|
||||
let mut f = std::fs::File::create(&tmp)
|
||||
.with_context(|| format!("create htpasswd tmp {}", tmp.display()))?;
|
||||
std::io::Write::write_all(&mut f, content.as_bytes())
|
||||
.and_then(|()| f.sync_all())
|
||||
.with_context(|| format!("write htpasswd tmp {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, &path)
|
||||
// Mode set on the TMP file for the same reason as in `write()`: the
|
||||
// rename carries it, and nginx reads this file as a different user.
|
||||
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o644))
|
||||
.with_context(|| format!("chmod htpasswd tmp {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, path)
|
||||
.with_context(|| format!("rename {} → {}", tmp.display(), path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -396,6 +406,27 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::lifecycle::MANAGER_NAME;
|
||||
|
||||
/// The htpasswd file is published `0644` whatever the umask, holds the
|
||||
/// lines newline-terminated, and leaves no temp file beside it.
|
||||
#[test]
|
||||
fn htpasswd_is_written_0644_with_no_temp_left() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("htpasswd");
|
||||
std::fs::write(&path, "stale\n").expect("seed");
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).expect("chmod");
|
||||
|
||||
htpasswd_write_at(&path, &["a:x".to_owned(), "b:y".to_owned()]).expect("write");
|
||||
|
||||
let mode = std::fs::metadata(&path).expect("stat").permissions().mode() & 0o777;
|
||||
assert_eq!(mode, 0o644);
|
||||
assert_eq!(std::fs::read_to_string(&path).expect("read"), "a:x\nb:y\n");
|
||||
let entries: Vec<_> = std::fs::read_dir(dir.path())
|
||||
.expect("readdir")
|
||||
.map(|e| e.expect("entry").file_name())
|
||||
.collect();
|
||||
assert_eq!(entries, [std::ffi::OsString::from("htpasswd")]);
|
||||
}
|
||||
|
||||
// ── legacy mode (frontend_dir = None) ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue