From a5d187e13d951dc09587a11a2e24ac889a3fe4b2 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 19 May 2026 14:04:10 -0500 Subject: [PATCH] Create secret files with private modes Previously our api key file and TLS key were both written with normal file permissions and then we later updated it 0o400. Now we make sure we create the file with 0o400 permissions so we never have a lapse where the file might be readable by someone we don't want. --- ldk-server/src/main.rs | 9 ++----- ldk-server/src/util/mod.rs | 54 ++++++++++++++++++++++++++++++++++++++ ldk-server/src/util/tls.rs | 7 +++-- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index ba64e7ce..2d4fb5f2 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -14,7 +14,6 @@ mod util; use std::collections::HashSet; use std::fs; -use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -56,6 +55,7 @@ use crate::util::metrics::Metrics; use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto}; use crate::util::systemd; use crate::util::tls::get_or_generate_tls_config; +use crate::util::write_new; const API_KEY_FILE: &str = "api_key"; @@ -834,12 +834,7 @@ fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result { let mut key_bytes = [0u8; 32]; getrandom::getrandom(&mut key_bytes).map_err(std::io::Error::other)?; - // Write the raw bytes to the file - fs::write(&api_key_path, key_bytes)?; - - // Set permissions to 0400 (read-only for owner) - let permissions = fs::Permissions::from_mode(0o400); - fs::set_permissions(&api_key_path, permissions)?; + write_new(&api_key_path, &key_bytes, 0o400)?; debug!("Generated new API key at {}", api_key_path.display()); Ok(key_bytes.to_lower_hex_string()) diff --git a/ldk-server/src/util/mod.rs b/ldk-server/src/util/mod.rs index a57dbd00..a08295e2 100644 --- a/ldk-server/src/util/mod.rs +++ b/ldk-server/src/util/mod.rs @@ -13,3 +13,57 @@ pub(crate) mod metrics; pub(crate) mod proto_adapter; pub(crate) mod systemd; pub(crate) mod tls; + +use std::fs::{self, OpenOptions}; +use std::io::{self, Write}; +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::path::Path; + +pub(crate) fn write_new(path: &Path, contents: &[u8], mode: u32) -> io::Result<()> { + let mut file = OpenOptions::new().create_new(true).write(true).mode(mode).open(path)?; + file.write_all(contents)?; + fs::set_permissions(path, fs::Permissions::from_mode(mode))?; + file.sync_all()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn write_new_sets_requested_mode_and_contents() { + let dir = test_dir("mode_and_contents"); + let path = dir.join("secret"); + + write_new(&path, b"secret-bytes", 0o400).unwrap(); + + assert_eq!(fs::read(&path).unwrap(), b"secret-bytes"); + assert_eq!(fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o400); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn write_new_does_not_replace_existing_file() { + let dir = test_dir("existing_file"); + let path = dir.join("secret"); + fs::write(&path, b"original").unwrap(); + + let err = write_new(&path, b"replacement", 0o400).unwrap_err(); + + assert_eq!(err.kind(), io::ErrorKind::AlreadyExists); + assert_eq!(fs::read(&path).unwrap(), b"original"); + + fs::remove_dir_all(dir).unwrap(); + } + + fn test_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir() + .join(format!("ldk-server-secure-file-test-{name}-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir(&dir).unwrap(); + dir + } +} diff --git a/ldk-server/src/util/tls.rs b/ldk-server/src/util/tls.rs index b7cc57b6..66086c6e 100644 --- a/ldk-server/src/util/tls.rs +++ b/ldk-server/src/util/tls.rs @@ -9,7 +9,7 @@ use std::fs; use std::net::IpAddr; -use std::os::unix::fs::PermissionsExt; +use std::path::Path; use base64::Engine; use ring::rand::SystemRandom; @@ -18,6 +18,7 @@ use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; use tokio_rustls::rustls::ServerConfig; use crate::util::config::TlsConfig; +use crate::util::write_new; // Issuer and Subject common name const ISSUER_NAME: &str = "localhost"; @@ -133,10 +134,8 @@ fn generate_self_signed_cert( let cert_pem = der_to_pem(&cert_der, PEM_CERT_BEGIN, PEM_CERT_END); let key_pem = der_to_pem(pkcs8_doc.as_ref(), PEM_KEY_BEGIN, PEM_KEY_END); - fs::write(key_path, &key_pem) + write_new(Path::new(key_path), key_pem.as_bytes(), 0o400) .map_err(|e| format!("Failed to write TLS key to '{key_path}': {e}"))?; - fs::set_permissions(key_path, fs::Permissions::from_mode(0o400)) - .map_err(|e| format!("Failed to set TLS key permissions for '{key_path}': {e}"))?; fs::write(cert_path, &cert_pem) .map_err(|e| format!("Failed to write TLS certificate to '{cert_path}': {e}"))?;