From 3ec467c2411550df7e66110bae84c889ca9b000e Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:21:28 +0200 Subject: [PATCH 1/2] confirm write permissions, save cert before completing wizard --- src/http.rs | 100 ++++++++++--------------------------------- src/setup.rs | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 79 deletions(-) diff --git a/src/http.rs b/src/http.rs index f1b8e2e..86621e5 100644 --- a/src/http.rs +++ b/src/http.rs @@ -24,8 +24,6 @@ use clap::crate_version; use defguard_version::{Version, server::DefguardVersionLayer}; use serde::Serialize; use tokio::{ - fs::OpenOptions, - io::AsyncWriteExt, sync::{broadcast, mpsc, oneshot}, task::JoinSet, }; @@ -53,10 +51,7 @@ const DEFGUARD_CORE_VERSION_HEADER: &str = "defguard-core-version"; const RATE_LIMITER_CLEANUP_PERIOD: Duration = Duration::from_secs(60); const X_FORWARDED_FOR: &str = "x-forwarded-for"; const X_POWERED_BY: &str = "x-powered-by"; -pub const GRPC_CERT_NAME: &str = "proxy_grpc_cert.pem"; -pub const GRPC_KEY_NAME: &str = "proxy_grpc_key.pem"; -pub const GRPC_CA_CERT_NAME: &str = "grpc_ca_cert.pem"; -pub const CORE_CLIENT_CERT_NAME: &str = "core_client_cert.pem"; +pub use crate::setup::{CORE_CLIENT_CERT_NAME, GRPC_CA_CERT_NAME, GRPC_CERT_NAME, GRPC_KEY_NAME}; #[derive(Clone)] pub(crate) struct AppState { @@ -181,7 +176,6 @@ async fn powered_by_header(mut response: Response) -> Response { } pub async fn run_setup(env_config: &EnvConfig, logs_rx: LogsReceiver) -> anyhow::Result { - let setup_server = ProxySetupServer::new(logs_rx); let cert_dir = Path::new(&env_config.cert_dir); if !cert_dir.exists() { tokio::fs::create_dir_all(cert_dir).await.map_err(|err| { @@ -196,13 +190,34 @@ pub async fn run_setup(env_config: &EnvConfig, logs_rx: LogsReceiver) -> anyhow: })?; #[cfg(unix)] tokio::fs::set_permissions(cert_dir, Permissions::from_mode(0o700)).await?; + } else { + // verify write access before starting the setup server + let test_path = cert_dir.join(".write_test"); + match tokio::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&test_path) + .await + { + Ok(_) => { + let _ = tokio::fs::remove_file(&test_path).await; + } + Err(err) if err.kind() == ErrorKind::PermissionDenied => { + anyhow::bail!( + "Certificate directory {} is not writable. Permission denied.", + cert_dir.display() + ); + } + Err(err) => return Err(err.into()), + } } - // Only attempt setup if not already configured info!( "No gRPC TLS certificates found at {}, new certificates will be obtained during setup", cert_dir.display() ); + let setup_server = ProxySetupServer::new(logs_rx, cert_dir.to_path_buf()); let tls_config = setup_server .await_initial_setup( SocketAddr::new( @@ -216,75 +231,6 @@ pub async fn run_setup(env_config: &EnvConfig, logs_rx: LogsReceiver) -> anyhow: .await?; info!("Generated new gRPC TLS certificates and signed by Defguard Core"); - let TlsConfig { - grpc_cert_pem, - grpc_key_pem, - grpc_ca_cert_pem, - core_client_cert_der, - } = &tls_config; - - let cert_path = cert_dir.join(GRPC_CERT_NAME); - let key_path = cert_dir.join(GRPC_KEY_NAME); - // Certificate and its key will be accessed only to this process's user. - let mut options = OpenOptions::new(); - options.write(true).create(true).truncate(true); - #[cfg(unix)] - options.mode(0o600); // rw------- - - // Write certificate to a file. - options - .clone() - .open(&cert_path) - .await? - .write_all(grpc_cert_pem.as_bytes()) - .await.map_err(|err| { - if err.kind() == ErrorKind::PermissionDenied { - anyhow::anyhow!( - "Cannot write certificate file {}. Permission denied for certificate directory {}.", - cert_path.display(), - cert_dir.display() - ) - } else { - err.into() - } - })?; - // Write key to a file. - options - .clone() - .open(&key_path) - .await? - .write_all(grpc_key_pem.as_bytes()) - .await - .map_err(|err| { - if err.kind() == ErrorKind::PermissionDenied { - anyhow::anyhow!( - "Cannot write key file {}. Permission denied for certificate directory {}.", - key_path.display(), - cert_dir.display() - ) - } else { - err.into() - } - })?; - // Write CA certificate to a file. - options - .clone() - .open(cert_dir.join(GRPC_CA_CERT_NAME)) - .await? - .write_all(grpc_ca_cert_pem.as_bytes()) - .await?; - // Write Core client certificate (PEM-encoded) to a file for serial pinning on restart. - let core_client_cert_pem = - defguard_certs::der_to_pem(core_client_cert_der, defguard_certs::PemLabel::Certificate) - .map_err(|err| { - anyhow::anyhow!("Failed to PEM-encode Core client certificate: {err}") - })?; - options - .open(cert_dir.join(CORE_CLIENT_CERT_NAME)) - .await? - .write_all(core_client_cert_pem.as_bytes()) - .await?; - Ok(tls_config) } diff --git a/src/setup.rs b/src/setup.rs index cc83824..9c8f1a9 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -1,5 +1,6 @@ use std::{ net::SocketAddr, + path::{Path, PathBuf}, sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, @@ -12,7 +13,11 @@ use defguard_version::{ server::{DefguardVersionLayer, grpc::DefguardVersionInterceptor}, }; use rustls_pki_types::{CertificateDer, UnixTime}; -use tokio::sync::{mpsc, oneshot}; +use tokio::{ + fs::OpenOptions, + io::AsyncWriteExt, + sync::{mpsc, oneshot}, +}; use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{Request, Response, Status, service::InterceptorLayer, transport::Server}; use webpki::{KeyUsage, anchor_from_trusted_cert}; @@ -27,6 +32,11 @@ use crate::{ const AUTH_HEADER: &str = "authorization"; +pub const GRPC_CERT_NAME: &str = "proxy_grpc_cert.pem"; +pub const GRPC_KEY_NAME: &str = "proxy_grpc_key.pem"; +pub const GRPC_CA_CERT_NAME: &str = "grpc_ca_cert.pem"; +pub const CORE_CLIENT_CERT_NAME: &str = "core_client_cert.pem"; + /// Verify that both `component_der` and `core_client_der` are signed by `ca_der`. /// /// Uses ECDSA P-256 via `aws-lc-rs` (Linux-only deployment; FIPS-capable). @@ -88,6 +98,96 @@ fn validate_cert_bundle( Ok(()) } +async fn save_tls_certs(tls_config: &TlsConfig, cert_dir: &Path) -> Result<(), String> { + let cert_path = cert_dir.join(GRPC_CERT_NAME); + let key_path = cert_dir.join(GRPC_KEY_NAME); + let ca_cert_path = cert_dir.join(GRPC_CA_CERT_NAME); + let client_cert_path = cert_dir.join(CORE_CLIENT_CERT_NAME); + + let mut options = OpenOptions::new(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + options.mode(0o600); // rw------- + + // PEM-encode the Core client certificate DER for serial pinning on restart. + let core_client_cert_pem = defguard_certs::der_to_pem( + &tls_config.core_client_cert_der, + defguard_certs::PemLabel::Certificate, + ) + .map_err(|err| format!("Failed to PEM-encode Core client certificate: {err}"))?; + + // Write component (server) certificate. + options + .clone() + .open(&cert_path) + .await + .map_err(|err| { + format!( + "Cannot open certificate file {}: {err}", + cert_path.display() + ) + })? + .write_all(tls_config.grpc_cert_pem.as_bytes()) + .await + .map_err(|err| { + format!( + "Cannot write certificate file {}: {err}", + cert_path.display() + ) + })?; + + // Write private key. + options + .clone() + .open(&key_path) + .await + .map_err(|err| format!("Cannot open key file {}: {err}", key_path.display()))? + .write_all(tls_config.grpc_key_pem.as_bytes()) + .await + .map_err(|err| format!("Cannot write key file {}: {err}", key_path.display()))?; + + // Write CA certificate. + options + .clone() + .open(&ca_cert_path) + .await + .map_err(|err| { + format!( + "Cannot open CA certificate file {}: {err}", + ca_cert_path.display() + ) + })? + .write_all(tls_config.grpc_ca_cert_pem.as_bytes()) + .await + .map_err(|err| { + format!( + "Cannot write CA certificate file {}: {err}", + ca_cert_path.display() + ) + })?; + + // Write Core client certificate (PEM) for serial pinning on restart. + options + .open(&client_cert_path) + .await + .map_err(|err| { + format!( + "Cannot open Core client certificate file {}: {err}", + client_cert_path.display() + ) + })? + .write_all(core_client_cert_pem.as_bytes()) + .await + .map_err(|err| { + format!( + "Cannot write Core client certificate file {}: {err}", + client_cert_path.display() + ) + })?; + + Ok(()) +} + pub(crate) struct ProxySetupServer { key_pair: Arc>>, logs_rx: LogsReceiver, @@ -95,6 +195,7 @@ pub(crate) struct ProxySetupServer { setup_tx: Arc>>>, setup_rx: Arc>>, adoption_expired: Arc, + cert_dir: Arc, } impl Clone for ProxySetupServer { @@ -106,12 +207,13 @@ impl Clone for ProxySetupServer { setup_tx: Arc::clone(&self.setup_tx), setup_rx: Arc::clone(&self.setup_rx), adoption_expired: Arc::clone(&self.adoption_expired), + cert_dir: Arc::clone(&self.cert_dir), } } } impl ProxySetupServer { - pub fn new(logs_rx: LogsReceiver) -> Self { + pub fn new(logs_rx: LogsReceiver, cert_dir: PathBuf) -> Self { let (setup_tx, setup_rx) = oneshot::channel(); Self { key_pair: Arc::new(Mutex::new(None)), @@ -120,6 +222,7 @@ impl ProxySetupServer { setup_tx: Arc::new(tokio::sync::Mutex::new(Some(setup_tx))), setup_rx: Arc::new(tokio::sync::Mutex::new(setup_rx)), adoption_expired: Arc::new(AtomicBool::new(false)), + cert_dir: Arc::new(cert_dir), } } @@ -490,6 +593,16 @@ impl proxy_setup_server::ProxySetup for ProxySetupServer { core_client_cert_der: bundle.core_client_cert_der, }; + debug!("Saving TLS certificate files to disk"); + if let Err(err) = save_tls_certs(&configuration, &self.cert_dir).await { + error!("Failed to save TLS certificates: {err}"); + self.clear_setup_session(); + return Err(Status::internal(format!( + "Failed to save TLS certificates: {err}" + ))); + } + debug!("TLS certificate files saved successfully"); + debug!("Passing configuration to gRPC server for finalization"); let Some(sender) = self.setup_tx.lock().await.take() else { error!("Setup channel sender already consumed"); From 4323bc00c830beca200f1e0ee5067c4c7847c84a Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:26:41 +0200 Subject: [PATCH 2/2] bump --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be35895..f87f53b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3803,9 +3803,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.12" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring",