write files potentially containing secrets as 0600

This commit is contained in:
mbecker20
2025-10-07 23:59:53 -07:00
parent cac1f0b42e
commit 561b490f26
21 changed files with 174 additions and 111 deletions

12
Cargo.lock generated
View File

@@ -1755,7 +1755,7 @@ dependencies = [
"anyhow",
"formatting",
"komodo_client",
"tokio",
"secret_file",
]
[[package]]
@@ -2871,6 +2871,7 @@ dependencies = [
"resolver_api",
"response",
"rustls 0.23.32",
"secret_file",
"serde",
"serde_json",
"serde_yaml_ng",
@@ -2926,6 +2927,7 @@ dependencies = [
"response",
"run_command",
"rustls 0.23.32",
"secret_file",
"serde",
"serde_json",
"serde_yaml_ng",
@@ -3297,6 +3299,7 @@ dependencies = [
"komodo_client",
"pem-rfc7468",
"pkcs8",
"secret_file",
"serde_json",
"snow",
"spki",
@@ -4661,6 +4664,13 @@ dependencies = [
"zeroize",
]
[[package]]
name = "secret_file"
version = "2.0.0-dev-42"
dependencies = [
"tokio",
]
[[package]]
name = "security-framework"
version = "2.11.1"

View File

@@ -25,6 +25,7 @@ periphery_client = { path = "client/periphery/rs" }
environment_file = { path = "lib/environment_file" }
environment = { path = "lib/environment" }
interpolate = { path = "lib/interpolate" }
secret_file = { path = "lib/secret_file" }
formatting = { path = "lib/formatting" }
transport = { path = "lib/transport" }
database = { path = "lib/database" }

View File

@@ -19,6 +19,7 @@ komodo_client = { workspace = true, features = ["mongo"] }
periphery_client.workspace = true
environment_file.workspace = true
interpolate.workspace = true
secret_file.workspace = true
formatting.workspace = true
transport.workspace = true
database.workspace = true

View File

@@ -142,15 +142,11 @@ impl Resolve<ExecuteArgs> for RunAction {
let file = format!("{}.ts", random_string(10));
let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize Action file parent directory {parent:?}"))?;
}
fs::write(&path, contents).await.with_context(|| {
format!("Failed to write action file to {path:?}")
})?;
secret_file::write_async(&path, contents)
.await
.with_context(|| {
format!("Failed to write action file to {path:?}")
})?;
let CoreConfig { ssl_enabled, .. } = core_config();

View File

@@ -272,8 +272,9 @@ async fn write_dockerfile_contents_git(
return Ok(update);
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
if let Err(e) = secret_file::write_async(&full_path, &contents)
.await
.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})
{

View File

@@ -206,15 +206,7 @@ async fn write_sync_file_contents_on_host(
.context("Invalid resource path")?;
let full_path = root.join(&resource_path).join(&file_path);
if let Some(parent) = full_path.parent() {
tokio::fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize resource file parent directory {parent:?}"
)
})?;
}
if let Err(e) = tokio::fs::write(&full_path, &contents)
if let Err(e) = secret_file::write_async(&full_path, &contents)
.await
.with_context(|| {
format!(
@@ -491,12 +483,7 @@ impl Resolve<WriteArgs> for CommitSync {
.sync_directory
.join(to_path_compatible_name(&sync.name))
.join(&resource_path);
if let Some(parent) = file_path.parent() {
tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize resource file parent directory {parent:?}"))?;
};
if let Err(e) = tokio::fs::write(&file_path, &res.toml)
if let Err(e) = secret_file::write_async(&file_path, &res.toml)
.await
.with_context(|| {
format!("Failed to write resource file to {file_path:?}",)

View File

@@ -20,6 +20,7 @@ periphery_client.workspace = true
environment_file.workspace = true
environment.workspace = true
interpolate.workspace = true
secret_file.workspace = true
formatting.workspace = true
transport.workspace = true
response.workspace = true

View File

@@ -26,14 +26,7 @@ pub async fn write_dockerfile(
.components()
.collect::<PathBuf>();
// Ensure parent directory exists
if let Some(parent) = full_dockerfile_path.parent() && !parent.exists() {
tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
}
tokio::fs::write(&full_dockerfile_path, dockerfile).await.with_context(|| {
secret_file::write_async(&full_dockerfile_path, dockerfile).await.with_context(|| {
format!(
"Failed to write dockerfile contents to {full_dockerfile_path:?}"
)
@@ -86,12 +79,14 @@ pub async fn parse_secret_args(
}
// Write the value to file to mount
let path = build_dir.join(variable);
tokio::fs::write(&path, value).await.with_context(|| {
format!(
"Failed to write build secret {variable} to {}",
path.display()
)
})?;
secret_file::write_async(&path, value).await.with_context(
|| {
format!(
"Failed to write build secret {variable} to {}",
path.display()
)
},
)?;
// Extend the command
write!(
&mut res,

View File

@@ -106,9 +106,13 @@ impl Resolve<super::Args> for WriteDockerfileContentsToHost {
.await
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
}
fs::write(&full_path, contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})?;
secret_file::write_async(&full_path, contents)
.await
.with_context(|| {
format!(
"Failed to write dockerfile contents to {full_path:?}"
)
})?;
Ok(Log::simple(
"Write dockerfile to host",
format!("dockerfile contents written to {full_path:?}"),

View File

@@ -1,3 +1,5 @@
use std::{borrow::Cow, path::PathBuf};
use anyhow::{Context, anyhow};
use command::{
run_komodo_command, run_komodo_command_with_sanitization,
@@ -18,8 +20,6 @@ use periphery_client::api::compose::*;
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
use shell_escape::unix::escape;
use std::{borrow::Cow, path::PathBuf};
use tokio::fs;
use crate::{
config::periphery_config,
@@ -160,12 +160,6 @@ impl Resolve<super::Args> for GetComposeContentsOnHost {
let run_directory =
root.join(&run_directory).components().collect::<PathBuf>();
if !run_directory.exists() {
fs::create_dir_all(&run_directory)
.await
.context("Failed to initialize run directory")?;
}
let mut res = GetComposeContentsOnHostResponse::default();
for file in file_paths {
@@ -173,11 +167,13 @@ impl Resolve<super::Args> for GetComposeContentsOnHost {
.join(&file.path)
.components()
.collect::<PathBuf>();
match fs::read_to_string(&full_path).await.with_context(|| {
format!(
"Failed to read compose file contents at {full_path:?}"
)
}) {
match tokio::fs::read_to_string(&full_path).await.with_context(
|| {
format!(
"Failed to read compose file contents at {full_path:?}"
)
},
) {
Ok(contents) => {
// The path we store here has to be the same as incoming file path in the array,
// in order for WriteComposeContentsToHost to write to the correct path.
@@ -227,17 +223,13 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
.join(file_path)
.components()
.collect::<PathBuf>();
// Ensure parent directory exists
if let Some(parent) = file_path.parent() {
fs::create_dir_all(&parent)
.await
.with_context(|| format!("Failed to initialize compose file parent directory {parent:?}"))?;
}
fs::write(&file_path, contents).await.with_context(|| {
format!(
"Failed to write compose file contents to {file_path:?}"
)
})?;
secret_file::write_async(&file_path, contents)
.await
.with_context(|| {
format!(
"Failed to write compose file contents to {file_path:?}"
)
})?;
Ok(Log::simple(
"Write contents to host",
format!("File contents written to {file_path:?}"),

View File

@@ -354,7 +354,7 @@ async fn write_stack_ui_defined(
.components()
.collect::<PathBuf>();
fs::write(&file_path, &stack.config.file_contents)
secret_file::write_async(&file_path, &stack.config.file_contents)
.await
.with_context(|| {
format!("Failed to write compose file to {file_path:?}")

View File

@@ -60,7 +60,7 @@ impl Resolve<super::Args> for RotateCorePublicKey {
};
SpkiPublicKey::from(self.public_key)
.write_pem(core_public_key_path)?;
.write_pem_sync(core_public_key_path)?;
core_public_keys().refresh();

View File

@@ -9,7 +9,7 @@ homepage.workspace = true
[dependencies]
komodo_client.workspace = true
secret_file.workspace = true
formatting.workspace = true
#
anyhow.workspace = true
tokio.workspace = true
anyhow.workspace = true

View File

@@ -32,19 +32,7 @@ pub async fn write_env_file(
.collect::<Vec<_>>()
.join("\n");
if let Some(parent) = env_file_path.parent()
&& let Err(e) = tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize environment file parent directory {parent:?}"))
{
logs.push(Log::error(
"Write Environment File",
format_serror(&e.into()),
));
return None;
}
if let Err(e) = tokio::fs::write(&env_file_path, contents)
if let Err(e) = secret_file::write_async(&env_file_path, contents)
.await
.with_context(|| {
format!("Failed to write environment file to {env_file_path:?}")

View File

@@ -7,7 +7,6 @@ use komodo_client::entities::{
RepoExecutionResponse, all_logs_success, update::Log,
};
use run_command::async_run_command;
use tokio::fs;
use crate::get_commit_hash_log;
@@ -35,12 +34,12 @@ pub async fn write_commit_file(
.collect::<PathBuf>();
if let Some(parent) = full_file_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
tokio::fs::create_dir_all(parent).await.with_context(|| {
format!("Failed to initialize file parent directory {parent:?}")
})?;
}
fs::write(&full_file_path, contents)
tokio::fs::write(&full_file_path, contents)
.await
.with_context(|| {
format!("Failed to write contents to {full_file_path:?}")

View File

@@ -9,6 +9,7 @@ homepage.workspace = true
[dependencies]
komodo_client.workspace = true
secret_file.workspace = true
#
pem-rfc7468.workspace = true
serde_json.workspace = true

View File

@@ -73,7 +73,7 @@ pub fn generate_write_keys(
let path = path.as_ref();
// Generate and write pems to path
let keys = EncodedKeyPair::generate()?;
keys.private.write_pem(path)?;
keys.public.write_pem(path.with_extension("pub"))?;
keys.private.write_pem_sync(path)?;
keys.public.write_pem_sync(path.with_extension("pub"))?;
Ok(keys)
}

View File

@@ -42,25 +42,32 @@ impl Pkcs8PrivateKey {
)
}
pub fn write_pem<P: AsRef<Path>>(
pub fn write_pem_sync(
&self,
path: P,
path: impl AsRef<Path>,
) -> anyhow::Result<()> {
let path = path.as_ref();
// Ensure the parent directory exists
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!(
"Failed to create private key parent directory {parent:?}"
)
})?;
}
tracing::info!("Writing private key to {path:?}");
std::fs::write(path, self.as_pem()).with_context(|| {
secret_file::write_sync(path, self.as_pem()).with_context(|| {
format!("Failed to write private key pem to {path:?}")
})
}
pub async fn write_pem_async(
&self,
path: impl AsRef<Path>,
) -> anyhow::Result<()> {
let path = path.as_ref();
// Ensure the parent directory exists
tracing::info!("Writing private key to {path:?}");
secret_file::write_async(path, self.as_pem())
.await
.with_context(|| {
format!("Failed to write private key pem to {path:?}")
})
}
/// - For raw bytes: converts to pkcs8 and returns
/// - For pkcs8 base64: clones and returns
/// - For pkcs8 base64 pem: unwraps and returns

View File

@@ -43,25 +43,30 @@ impl SpkiPublicKey {
)
}
pub fn write_pem<P: AsRef<Path>>(
pub fn write_pem_sync(
&self,
path: P,
path: impl AsRef<Path>,
) -> anyhow::Result<()> {
let path = path.as_ref();
// Ensure the parent directory exists
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!(
"Failed to create private key parent directory {parent:?}"
)
})?;
}
tracing::info!("Writing public key to {path:?}");
std::fs::write(path, self.as_pem()).with_context(|| {
secret_file::write_sync(path, self.as_pem()).with_context(|| {
format!("Failed to write private key pem to {path:?}")
})
}
pub async fn write_pem_async(
&self,
path: impl AsRef<Path>,
) -> anyhow::Result<()> {
let path = path.as_ref();
tracing::info!("Writing public key to {path:?}");
secret_file::write_async(path, self.as_pem())
.await
.with_context(|| {
format!("Failed to write private key pem to {path:?}")
})
}
/// Accepts pem rfc7468 (openssl) or base64 der (second line of rfc7468 pem).
pub fn from_maybe_pem(
public_key_maybe_pem: &str,

View File

@@ -0,0 +1,11 @@
[package]
name = "secret_file"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
[dependencies]
tokio.workspace = true

View File

@@ -0,0 +1,64 @@
use std::path::Path;
/// Writes data to path, setting permissions to 0600.
/// `std::fs` sync version.
///
/// Also ensures parent directory exists.
pub fn write_sync(
path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
) -> std::io::Result<()> {
use std::{io::Write, os::unix::fs::OpenOptionsExt};
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
// Only sets mode if file is created.
// This leaves existing permissions intact.
.mode(0o600)
.open(path)?;
file.write_all(contents.as_ref())?;
file.flush()?;
Ok(())
}
/// Writes data to path, setting permissions to 0600.
/// `tokio::fs` async version.
///
/// Also ensures parent directory exists.
pub async fn write_async(
path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
let path = path.as_ref();
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let mut file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
// Only sets mode if file is created.
// This leaves existing permissions intact.
.mode(0o600)
.open(path)
.await?;
file.write_all(contents.as_ref()).await?;
file.flush().await?;
Ok(())
}