From c14b8b323a7ef87679d7974648fa911f752b9b1e Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 30 Jun 2026 18:10:48 +0530 Subject: [PATCH 1/2] fix(ssl): warn when the letsencrypt account key is missing but cert state exists --- src/helper/Site_Letsencrypt.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/helper/Site_Letsencrypt.php b/src/helper/Site_Letsencrypt.php index 0669eaf1..8542d5ea 100644 --- a/src/helper/Site_Letsencrypt.php +++ b/src/helper/Site_Letsencrypt.php @@ -119,6 +119,14 @@ function __construct() { private function setAcmeClient() { if ( ! $this->repository->hasAccountKeyPair() ) { + // A missing account key alongside existing LE domain state means the key was lost (host migration / snapshot restore), + // not a first run. Generating a new one silently orphans the old LE registration, so warn before regenerating. + if ( $this->hasExistingLetsencryptState() ) { + \EE::warning( 'Let\'s Encrypt account key not found, but existing certificate state was detected under ' . $this->conf_dir . ' — the account key appears to have been lost (e.g. host migration or snapshot restore).' ); + \EE::warning( 'A new Let\'s Encrypt account will be registered. The previous account is now orphaned, so existing certificates will NOT renew under it until they are re-issued.' ); + \EE::warning( 'To preserve the existing account, restore a backup of ' . $this->conf_dir . '/account/ before re-running.' ); + } + \EE::debug( 'No account key pair was found, generating one.' ); \EE::debug( 'Generating a key pair' ); @@ -140,6 +148,23 @@ private function setAcmeClient() { } + /** + * Cheap, DB-free check for pre-existing Let's Encrypt state on disk. + * + * Looks only at AcmePhp's own per-domain dirs under acme-conf (var/{domain} = orders/challenges/DN, + * certs/{domain} = LE keypairs/certs). These are written solely by AcmePhp, so their presence proves + * the account key existed before. We deliberately ignore services/nginx-proxy/certs/, which also holds + * custom/self-signed certs and would false-positive on a host that never used Let's Encrypt. + * + * @return bool True if prior LE domain state exists. + */ + private function hasExistingLetsencryptState() { + $var_domains = glob( $this->conf_dir . '/var/*', GLOB_ONLYDIR ); + $cert_domains = glob( $this->conf_dir . '/certs/*', GLOB_ONLYDIR ); + + return ! empty( $var_domains ) || ! empty( $cert_domains ); + } + private function setRepository( $enable_backup = false ) { $this->serializer ?? $this->serializer = new Serializer( [ new PemNormalizer(), new GetSetMethodNormalizer() ], From a62ec2c0d4915f64751ae12f4dd728c734938d95 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 24 Sep 2026 10:39:05 +0000 Subject: [PATCH 2/2] fix(ssl): correct the lost account key warning text Every LE issuance and renewal goes through init_le(), which calls register() with the current key first, so a regenerated key just becomes a new account and renewals keep working. The old warning claimed existing certificates would not renew and that an account "will be registered", which is also wrong for commands that never register (site delete, alias update, ssl-verify). Only challenges still pending under the old account are affected. Also shorten the comments. --- src/helper/Site_Letsencrypt.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/helper/Site_Letsencrypt.php b/src/helper/Site_Letsencrypt.php index 8542d5ea..b2e8f4d5 100644 --- a/src/helper/Site_Letsencrypt.php +++ b/src/helper/Site_Letsencrypt.php @@ -119,12 +119,11 @@ function __construct() { private function setAcmeClient() { if ( ! $this->repository->hasAccountKeyPair() ) { - // A missing account key alongside existing LE domain state means the key was lost (host migration / snapshot restore), - // not a first run. Generating a new one silently orphans the old LE registration, so warn before regenerating. + // Missing key plus existing LE domain state means the key was lost, not a first run; warn before regenerating. if ( $this->hasExistingLetsencryptState() ) { - \EE::warning( 'Let\'s Encrypt account key not found, but existing certificate state was detected under ' . $this->conf_dir . ' — the account key appears to have been lost (e.g. host migration or snapshot restore).' ); - \EE::warning( 'A new Let\'s Encrypt account will be registered. The previous account is now orphaned, so existing certificates will NOT renew under it until they are re-issued.' ); - \EE::warning( 'To preserve the existing account, restore a backup of ' . $this->conf_dir . '/account/ before re-running.' ); + \EE::warning( 'Let\'s Encrypt account key not found, but existing certificate state was detected under ' . $this->conf_dir . '. The key appears to have been lost (e.g. host migration or snapshot restore), so a new one is being generated.' ); + \EE::warning( 'Existing certificates stay valid and will be renewed under a new Let\'s Encrypt account, but challenges still pending under the old account cannot be completed and must be restarted.' ); + \EE::warning( 'To keep the old account, restore ' . $this->conf_dir . '/account/ from a backup before the next SSL operation.' ); } \EE::debug( 'No account key pair was found, generating one.' ); @@ -149,12 +148,8 @@ private function setAcmeClient() { } /** - * Cheap, DB-free check for pre-existing Let's Encrypt state on disk. - * - * Looks only at AcmePhp's own per-domain dirs under acme-conf (var/{domain} = orders/challenges/DN, - * certs/{domain} = LE keypairs/certs). These are written solely by AcmePhp, so their presence proves - * the account key existed before. We deliberately ignore services/nginx-proxy/certs/, which also holds - * custom/self-signed certs and would false-positive on a host that never used Let's Encrypt. + * Checks for AcmePhp's per-domain dirs under acme-conf, which are only created after an account key exists. + * nginx-proxy/certs/ is ignored as it also holds custom/self-signed certs. * * @return bool True if prior LE domain state exists. */