From ee5664c7d74ddcaeecb49529315b711d341187d7 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 30 Jun 2026 16:29:44 +0530 Subject: [PATCH 1/3] fix(ssl): deploy nginx-proxy certs atomically with checked copies --- src/helper/Site_Letsencrypt.php | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/helper/Site_Letsencrypt.php b/src/helper/Site_Letsencrypt.php index 0669eaf1..dd5c1275 100644 --- a/src/helper/Site_Letsencrypt.php +++ b/src/helper/Site_Letsencrypt.php @@ -502,9 +502,34 @@ private function moveCertsToNginxProxy( string $domain ) { $crt_dest_file = EE_ROOT_DIR . '/services/nginx-proxy/certs/' . $domain . '.crt'; $chain_dest_file = EE_ROOT_DIR . '/services/nginx-proxy/certs/' . $domain . '.chain.pem'; - copy( $key_source_file, $key_dest_file ); - copy( $crt_source_file, $crt_dest_file ); - copy( $chain_source_file, $chain_dest_file ); + // Copy each source to a temp file in the destination dir, then rename into place. Each rename() + // is atomic per-file on the same filesystem, so a failed copy (disk full, permissions, crash) + // can never leave a half-written live cert/key. The renames are not collectively atomic and we do + // not roll back an already-renamed file; on any failure we clean up the temps and throw. + $copy_map = [ + $key_source_file => $key_dest_file, + $crt_source_file => $crt_dest_file, + $chain_source_file => $chain_dest_file, + ]; + + // $temp_files maps temp path => final destination path. + $temp_files = []; + foreach ( $copy_map as $source => $dest ) { + $temp = $dest . '.tmp'; + if ( ! copy( $source, $temp ) ) { + // Include the current temp: a failed copy may still have created a partial file. + array_map( 'unlink', array_filter( array_merge( array_keys( $temp_files ), [ $temp ] ), 'file_exists' ) ); + throw new \Exception( sprintf( 'Failed to copy certificate file %s to %s.', $source, $temp ) ); + } + $temp_files[ $temp ] = $dest; + } + + foreach ( $temp_files as $temp => $dest ) { + if ( ! rename( $temp, $dest ) ) { + array_map( 'unlink', array_filter( array_keys( $temp_files ), 'file_exists' ) ); + throw new \Exception( sprintf( 'Failed to move certificate file %s to %s.', $temp, $dest ) ); + } + } } /** From 13ffc46a2ff313ea340bdadf2c84b3f29b957656 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 24 Sep 2026 10:38:40 +0000 Subject: [PATCH 2/3] fix(ssl): keep existing cert file mode on atomic deploy rename() replaces the live file with a fresh temp created under the umask, so a renewal reset any tightened mode on the deployed key/cert (e.g. 0600 to 0644). The previous in-place copy() kept it; copy the live file's mode onto the temp before renaming. --- src/helper/Site_Letsencrypt.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/helper/Site_Letsencrypt.php b/src/helper/Site_Letsencrypt.php index dd5c1275..2e13d7c4 100644 --- a/src/helper/Site_Letsencrypt.php +++ b/src/helper/Site_Letsencrypt.php @@ -522,6 +522,10 @@ private function moveCertsToNginxProxy( string $domain ) { throw new \Exception( sprintf( 'Failed to copy certificate file %s to %s.', $source, $temp ) ); } $temp_files[ $temp ] = $dest; + // Keep the live file's mode on renewal, as the previous in-place copy() did. + if ( file_exists( $dest ) ) { + chmod( $temp, fileperms( $dest ) & 0777 ); + } } foreach ( $temp_files as $temp => $dest ) { From db1a89f64aac928292d4ca2f449132409250745f Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 24 Sep 2026 10:39:02 +0000 Subject: [PATCH 3/3] style(ssl): trim cert deploy comments --- src/helper/Site_Letsencrypt.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/helper/Site_Letsencrypt.php b/src/helper/Site_Letsencrypt.php index 2e13d7c4..37ce0b6a 100644 --- a/src/helper/Site_Letsencrypt.php +++ b/src/helper/Site_Letsencrypt.php @@ -502,17 +502,14 @@ private function moveCertsToNginxProxy( string $domain ) { $crt_dest_file = EE_ROOT_DIR . '/services/nginx-proxy/certs/' . $domain . '.crt'; $chain_dest_file = EE_ROOT_DIR . '/services/nginx-proxy/certs/' . $domain . '.chain.pem'; - // Copy each source to a temp file in the destination dir, then rename into place. Each rename() - // is atomic per-file on the same filesystem, so a failed copy (disk full, permissions, crash) - // can never leave a half-written live cert/key. The renames are not collectively atomic and we do - // not roll back an already-renamed file; on any failure we clean up the temps and throw. + // Stage temps in the destination dir and rename() them in, so a failed copy never leaves a half-written live key/cert. + // Each rename is atomic, the set is not; an already-renamed file is not rolled back. $copy_map = [ $key_source_file => $key_dest_file, $crt_source_file => $crt_dest_file, $chain_source_file => $chain_dest_file, ]; - // $temp_files maps temp path => final destination path. $temp_files = []; foreach ( $copy_map as $source => $dest ) { $temp = $dest . '.tmp';