From f3ea0b538f2b93d2fd22219acbd2fc3d5ca11bac Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 25 Sep 2026 09:47:01 +0000 Subject: [PATCH 1/3] fix(migration): don't apply wildcard auth files on the old nginx-proxy template The auth files migration runs before the image migration, and reloaded the proxy right after writing the new _wildcard.X files. The old nginx-proxy template applies _wildcard.X to every subdomain of X without its own file, so a sibling site such as shop. asked for the multisite's password until the proxy was recreated, and indefinitely when the image pull failed. The migration now backs up htpasswd/ and vhost.d/*_acl to /opt/easyengine/.backup/auth-migration-/ and reloads the proxy only when the running one has the new template (its nginx.tmpl defines the acl block); otherwise the image migration's recreated proxy renders the files. down() restores the backup exactly, including removing files it added, and reloads the proxy if it runs. The backup is kept after a successful upgrade for a manual downgrade. --- ...uth-command_regenerate_site_auth_files.php | 163 +++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php index 1b3740f..0c540fa 100644 --- a/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php +++ b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php @@ -12,6 +12,9 @@ class RegenerateSiteAuthFiles extends Base { private $sites; + /** @var string|null Backup of `htpasswd/` and `vhost.d/*_acl` taken by up(). */ + private $auth_backup; + public function __construct() { parent::__construct(); @@ -36,6 +39,10 @@ public function up() { return; } + $this->auth_backup = $this->backup_auth_files(); + + $new_template = $this->proxy_has_new_template(); + foreach ( $this->sites as $site ) { try { generate_site_auth_files( $site->site_url, $site ); @@ -45,12 +52,164 @@ public function up() { } } - \EE\Site\Utils\reload_global_nginx_proxy(); + if ( $new_template ) { + \EE\Site\Utils\reload_global_nginx_proxy(); + } else { + // The old template applies `_wildcard.X` to every subdomain of X; the image migration recreates the proxy with the new one. + EE::debug( 'Not reloading nginx-proxy: it runs the old template, the image migration recreates it.' ); + } } /** - * Not reverted. The files need the nginx-proxy image of the same release: the previous image also applies `_wildcard.*` files to sibling sites. + * Restores the files saved by up() exactly, including removing files it added. + * + * @throws \Exception */ public function down() { + + if ( empty( $this->auth_backup ) || ! is_dir( $this->auth_backup ) ) { + EE::debug( 'No site auth files backup to restore.' ); + + return; + } + + foreach ( $this->get_auth_dirs() as $name => $dir ) { + $this->restore_dir( $this->auth_backup . '/' . $name, $dir, 'vhost.d' === $name ); + } + + if ( 'running' === \EE_DOCKER::container_status( EE_PROXY_TYPE ) ) { + \EE\Site\Utils\reload_global_nginx_proxy(); + } + + $this->fs->remove( $this->auth_backup ); + EE::debug( 'Restored the site auth files from ' . $this->auth_backup ); + $this->auth_backup = null; + } + + /** + * @return array Backup subdirectory name => proxy directory. + */ + private function get_auth_dirs() { + + return [ + 'htpasswd' => EE_ROOT_DIR . '/services/nginx-proxy/htpasswd', + 'vhost.d' => EE_ROOT_DIR . '/services/nginx-proxy/vhost.d', + ]; + } + + /** + * Only `*_acl` files of vhost.d are written by auth-command. + * + * @param string $file File name. + * @param bool $acl_only Whether only ACL files are handled. + * + * @return bool + */ + private function is_handled_file( string $file, bool $acl_only ) { + + return ! $acl_only || '_acl' === substr( $file, -4 ); + } + + /** + * Copies `htpasswd/` and `vhost.d/*_acl` to a new directory under EE_BACKUP_DIR. + * + * Kept after a successful upgrade: downgrading needs these files, the old nginx-proxy leaks the new `_wildcard.X` files to sibling sites. + * + * @return string Backup directory. + * @throws \Exception + */ + private function backup_auth_files() { + + $backup = EE_BACKUP_DIR . '/auth-migration-' . date( 'Ymd-His' ); + if ( file_exists( $backup ) ) { + $backup .= '-' . getmypid(); + } + + try { + // Holds password hashes. + $this->fs->mkdir( $backup, 0700 ); + foreach ( $this->get_auth_dirs() as $name => $dir ) { + $this->fs->mkdir( $backup . '/' . $name, 0700 ); + if ( ! is_dir( $dir ) ) { + continue; + } + foreach ( scandir( $dir ) as $file ) { + if ( ! is_file( $dir . '/' . $file ) || ! $this->is_handled_file( $file, 'vhost.d' === $name ) ) { + continue; + } + $this->copy_file( $dir . '/' . $file, $backup . '/' . $name . '/' . $file ); + } + } + } catch ( \Throwable $e ) { + $this->fs->remove( $backup ); + throw new \Exception( 'Could not back up the site auth files: ' . $e->getMessage() ); + } + + EE::debug( "Backed up the site auth files to $backup" ); + + return $backup; + } + + /** + * Makes the handled files of $dir identical to $backup_dir. + * + * @param string $backup_dir Backup directory. + * @param string $dir Proxy directory. + * @param bool $acl_only Whether only ACL files are handled. + */ + private function restore_dir( string $backup_dir, string $dir, bool $acl_only ) { + + if ( ! is_dir( $dir ) || ! is_dir( $backup_dir ) ) { + return; + } + + foreach ( scandir( $dir ) as $file ) { + if ( is_file( $dir . '/' . $file ) && $this->is_handled_file( $file, $acl_only ) && ! file_exists( $backup_dir . '/' . $file ) ) { + $this->fs->remove( $dir . '/' . $file ); + } + } + + foreach ( scandir( $backup_dir ) as $file ) { + if ( ! is_file( $backup_dir . '/' . $file ) ) { + continue; + } + // Renamed into place so the proxy never reads a partial file. + $tmp = $dir . '/.' . $file . '.restore'; + $this->copy_file( $backup_dir . '/' . $file, $tmp ); + $this->fs->rename( $tmp, $dir . '/' . $file, true ); + } + } + + /** + * Copies a file with its mode and modification time. + * + * @param string $source Source path. + * @param string $target Target path. + */ + private function copy_file( string $source, string $target ) { + + $this->fs->copy( $source, $target, true ); + $this->fs->chmod( $target, fileperms( $source ) & 0777 ); + $this->fs->touch( $target, filemtime( $source ) ); + } + + /** + * Checks whether the running nginx-proxy has the template that applies `_wildcard.X` files only to `*.X` hosts. + * + * @return bool False when the proxy isn't running or runs an older template. + */ + private function proxy_has_new_template() { + + if ( 'running' !== \EE_DOCKER::container_status( EE_PROXY_TYPE ) ) { + EE::debug( 'nginx-proxy is not running: treating its template as the old one.' ); + + return false; + } + + $check = EE::launch( sprintf( 'docker exec %s grep -c %s /app/nginx.tmpl', EE_PROXY_TYPE, escapeshellarg( 'define "acl"' ) ) ); + $new = 0 === $check->return_code && (int) trim( $check->stdout ) > 0; + EE::debug( 'nginx-proxy template: ' . ( $new ? 'new (has the acl block)' : 'old (no acl block)' ) ); + + return $new; } } From 421286127e97961602f66ab612550220d44b0f8f Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 25 Sep 2026 11:34:05 +0000 Subject: [PATCH 2/3] fix(migration): stage wildcard auth files until the new nginx-proxy runs Skipping the reload didn't close the leak: the old nginx-proxy's docker-gen re-renders its template on any container event on the host, e.g. the cron container restarting, and that template applies _wildcard.X to every subdomain of X without its own file. Once the migration had written _wildcard.* files into the proxy's mounts, a sibling site such as shop. asked for the multisite's password until the proxy was recreated, until down() restored the files after a failed pull, or until the retry when ee was killed mid-pull. When the running proxy has the old template, or isn't running, the migration now writes only the exact-host htpasswd and ACL files, which the old template handles correctly, and stages the _wildcard.* ones in /opt/easyengine/.staging/auth-wildcard/ (outside the mounts, mode 0700). They are moved into place (temp file + rename) and the proxy is reloaded by the after_docker_image_migration hook once the new proxy runs, or by the first ee run after the migration completed (find_command_to_run_pre), so a killed or failed upgrade heals on the retry. A site whose auth changed since staging, e.g. through the older ee after an interrupted upgrade, gets its files regenerated instead, and files of deleted sites are dropped. down() discards the staging dir. With the new template, nothing changes: everything is written and the proxy reloaded. Until promotion, subdomains stay unprotected as in 4.12.0, and the old template never sees a _wildcard.* file. --- ...uth-command_regenerate_site_auth_files.php | 43 ++--- src/auth-utils.php | 162 ++++++++++++++++-- src/helper/hooks.php | 41 +++++ 3 files changed, 202 insertions(+), 44 deletions(-) diff --git a/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php index 0c540fa..84bb033 100644 --- a/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php +++ b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php @@ -7,6 +7,8 @@ use EE\Model\Site; use function EE\Auth\Utils\generate_site_auth_files; use function EE\Auth\Utils\generate_site_whitelist; +use function EE\Auth\Utils\get_wildcard_staging_dir; +use function EE\Auth\Utils\proxy_has_acl_template; class RegenerateSiteAuthFiles extends Base { @@ -41,12 +43,19 @@ public function up() { $this->auth_backup = $this->backup_auth_files(); - $new_template = $this->proxy_has_new_template(); + $new_template = proxy_has_acl_template(); + $stage = get_wildcard_staging_dir(); + $this->fs->remove( $stage ); + + // The old template applies `_wildcard.X` to every subdomain of X, even on a container event without a reload, so those files wait outside its mounts. + if ( ! $new_template ) { + $this->fs->mkdir( [ dirname( $stage ), $stage, "$stage/htpasswd", "$stage/vhost.d" ], 0700 ); + } foreach ( $this->sites as $site ) { try { - generate_site_auth_files( $site->site_url, $site ); - generate_site_whitelist( $site->site_url, $site ); + generate_site_auth_files( $site->site_url, $site, [], $new_template ? '' : $stage ); + generate_site_whitelist( $site->site_url, $site, [], $new_template ? '' : $stage ); } catch ( \Throwable $e ) { EE::warning( sprintf( 'Could not regenerate the auth files of %s: %s', $site->site_url, $e->getMessage() ) ); } @@ -55,18 +64,20 @@ public function up() { if ( $new_template ) { \EE\Site\Utils\reload_global_nginx_proxy(); } else { - // The old template applies `_wildcard.X` to every subdomain of X; the image migration recreates the proxy with the new one. - EE::debug( 'Not reloading nginx-proxy: it runs the old template, the image migration recreates it.' ); + // Promoted by the after_docker_image_migration hook, or on a later run, once the new proxy runs. + EE::debug( "Staged the wildcard auth files in $stage; nginx-proxy runs the old template." ); } } /** - * Restores the files saved by up() exactly, including removing files it added. + * Discards the staged wildcard files and restores the files saved by up() exactly, including removing files it added. * * @throws \Exception */ public function down() { + $this->fs->remove( get_wildcard_staging_dir() ); + if ( empty( $this->auth_backup ) || ! is_dir( $this->auth_backup ) ) { EE::debug( 'No site auth files backup to restore.' ); @@ -192,24 +203,4 @@ private function copy_file( string $source, string $target ) { $this->fs->chmod( $target, fileperms( $source ) & 0777 ); $this->fs->touch( $target, filemtime( $source ) ); } - - /** - * Checks whether the running nginx-proxy has the template that applies `_wildcard.X` files only to `*.X` hosts. - * - * @return bool False when the proxy isn't running or runs an older template. - */ - private function proxy_has_new_template() { - - if ( 'running' !== \EE_DOCKER::container_status( EE_PROXY_TYPE ) ) { - EE::debug( 'nginx-proxy is not running: treating its template as the old one.' ); - - return false; - } - - $check = EE::launch( sprintf( 'docker exec %s grep -c %s /app/nginx.tmpl', EE_PROXY_TYPE, escapeshellarg( 'define "acl"' ) ) ); - $new = 0 === $check->return_code && (int) trim( $check->stdout ) > 0; - EE::debug( 'nginx-proxy template: ' . ( $new ? 'new (has the acl block)' : 'old (no acl block)' ) ); - - return $new; - } } diff --git a/src/auth-utils.php b/src/auth-utils.php index 420abc0..9f308ec 100644 --- a/src/auth-utils.php +++ b/src/auth-utils.php @@ -6,6 +6,7 @@ use EE; use EE\Model\Auth; use EE\Model\Option; +use EE\Model\Site; use EE\Model\Whitelist; use Symfony\Component\Filesystem\Filesystem; use function EE\Service\Utils\ensure_global_network_initialized; @@ -158,30 +159,32 @@ function remove_proxy_file( string $dir, string $name ): bool { } /** - * Copies a file inside a proxy directory to other names in the same directory. + * Copies a file inside a proxy directory to other names in the same directory, or in $target_dir. * - * @param string $dir Directory path. - * @param string $source Source file name. - * @param array $targets Target file names. + * @param string $dir Directory path. + * @param string $source Source file name. + * @param array $targets Target file names. + * @param string $target_dir Target directory, $dir if empty. */ -function copy_proxy_file( string $dir, string $source, array $targets ) { +function copy_proxy_file( string $dir, string $source, array $targets, string $target_dir = '' ) { - $fs = new Filesystem(); - $mode = fileperms( $dir . '/' . $source ) & 0777; + $fs = new Filesystem(); + $mode = fileperms( $dir . '/' . $source ) & 0777; + $target_dir = '' === $target_dir ? $dir : $target_dir; foreach ( $targets as $target ) { - if ( ! is_proxy_file_name( $target ) || $target === $source ) { + if ( ! is_proxy_file_name( $target ) || ( $target === $source && $target_dir === $dir ) ) { continue; } // Built under a name no host matches, then renamed, so the proxy never reads a partial copy. $tmp = '.' . $target . '.tmp'; try { - $fs->copy( $dir . '/' . $source, $dir . '/' . $tmp, true ); + $fs->copy( $dir . '/' . $source, $target_dir . '/' . $tmp, true ); // Don't depend on the umask: nginx workers read these files. - $fs->chmod( $dir . '/' . $tmp, $mode ); - $fs->rename( $dir . '/' . $tmp, $dir . '/' . $target, true ); + $fs->chmod( $target_dir . '/' . $tmp, $mode ); + $fs->rename( $target_dir . '/' . $tmp, $target_dir . '/' . $target, true ); } catch ( \Exception $e ) { - remove_proxy_file( $dir, $tmp ); + remove_proxy_file( $target_dir, $tmp ); EE::warning( sprintf( 'Could not copy %s to %s, so it was left unchanged.', $source, $target ) ); } } @@ -215,14 +218,21 @@ function remove_auth_files( array $domains ): bool { * @param string $site_url URL of site. * @param \EE\Model\Site|null $site_data Site model. * @param array $extra_aliases Alias domains not saved on the site yet, e.g. ones about to be added. + * @param string $stage_dir If set, `_wildcard.*` files are written to its `htpasswd/` instead of the proxy. * * @throws \Exception */ -function generate_site_auth_files( string $site_url, $site_data = null, array $extra_aliases = [] ) { +function generate_site_auth_files( string $site_url, $site_data = null, array $extra_aliases = [], string $stage_dir = '' ) { $dir = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd'; $domains = get_site_auth_domains( $site_url, $site_data, $extra_aliases ); $site_auths = Auth::where( 'site_url', $site_url ); + $staged = '' === $stage_dir ? [] : array_filter( $domains, __NAMESPACE__ . '\is_wildcard_auth_name' ); + + // A staged name must not stay live either, its staged copy replaces it. + foreach ( $staged as $domain ) { + remove_proxy_file( $dir, $domain ); + } // Without site entries the proxy falls back to the global `default` file. if ( empty( $site_auths ) ) { @@ -237,7 +247,8 @@ function generate_site_auth_files( string $site_url, $site_data = null, array $e // If it can't be rewritten (e.g. the proxy is stopped during an upgrade), still spread the existing file to the other domains. if ( write_htpasswd_file( $source, array_merge( Auth::get_global_auths(), $site_auths ) ) || is_file( $dir . '/' . $source ) ) { - copy_proxy_file( $dir, $source, $domains ); + copy_proxy_file( $dir, $source, array_diff( $domains, $staged ) ); + copy_proxy_file( $dir, $source, $staged, $stage_dir . '/htpasswd' ); } } @@ -380,21 +391,24 @@ function get_site_whitelist_ips( string $site_url ): array { * @param string $site_url URL of site, `default` for global. * @param \EE\Model\Site|null $site_data Site model. * @param array $extra_aliases Alias domains not saved on the site yet, e.g. ones about to be added. + * @param string $stage_dir If set, `_wildcard.*` files are written to its `vhost.d/` instead of the proxy. * * @throws \Exception */ -function generate_site_whitelist( string $site_url, $site_data = null, array $extra_aliases = [] ) { +function generate_site_whitelist( string $site_url, $site_data = null, array $extra_aliases = [], string $stage_dir = '' ) { $dir = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d'; $domains = get_site_auth_domains( $site_url, $site_data, $extra_aliases ); $ips = get_site_whitelist_ips( $site_url ); foreach ( $domains as $domain ) { + $staged = '' !== $stage_dir && is_wildcard_auth_name( $domain ); // Without site entries the proxy falls back to `default_acl`. - if ( empty( $ips ) ) { + if ( empty( $ips ) || $staged ) { remove_proxy_file( $dir, $domain . '_acl' ); - } else { - put_ips_to_file( $dir . '/' . $domain . '_acl', $ips ); + } + if ( ! empty( $ips ) ) { + put_ips_to_file( ( $staged ? $stage_dir . '/vhost.d' : $dir ) . '/' . $domain . '_acl', $ips ); } } } @@ -418,3 +432,115 @@ function put_ips_to_file( string $file, array $ips ) { $file_content .= 'deny all;'; ( new Filesystem() )->dumpFile( $file, $file_content ); } + +/** + * Checks whether a htpasswd/ACL file name is a `*.X` one. + * + * @param string $name File name as returned by get_site_auth_domains(). + * + * @return bool + */ +function is_wildcard_auth_name( string $name ): bool { + + return 0 === strpos( $name, '_wildcard.' ); +} + +/** + * Directory, outside the proxy's mounts, where the auth migration stages `_wildcard.*` files while the old nginx-proxy template runs. + * + * @return string + */ +function get_wildcard_staging_dir(): string { + + return EE_ROOT_DIR . '/.staging/auth-wildcard'; +} + +/** + * Checks whether the running nginx-proxy has the template that applies `_wildcard.X` files only to `*.X` hosts. + * + * @return bool False when the proxy isn't running or runs an older template. + */ +function proxy_has_acl_template(): bool { + + if ( 'running' !== \EE_DOCKER::container_status( EE_PROXY_TYPE ) ) { + EE::debug( 'nginx-proxy is not running: treating its template as the old one.' ); + + return false; + } + + $check = EE::launch( sprintf( 'docker exec %s grep -c %s /app/nginx.tmpl', EE_PROXY_TYPE, escapeshellarg( 'define "acl"' ) ) ); + $new = 0 === $check->return_code && (int) trim( $check->stdout ) > 0; + EE::debug( 'nginx-proxy template: ' . ( $new ? 'new (has the acl block)' : 'old (no acl block)' ) ); + + return $new; +} + +/** + * Checks whether a staged file matches the site's own live file: both absent, or both with the same content. + * + * @param string $live Live file path. + * @param string $staged Staged file path. + * + * @return bool + */ +function staged_file_is_current( string $live, string $staged ): bool { + + if ( ! is_file( $live ) || ! is_file( $staged ) ) { + return is_file( $live ) === is_file( $staged ); + } + + return file_get_contents( $live ) === file_get_contents( $staged ); +} + +/** + * Moves the `_wildcard.*` files staged by the auth migration into the proxy once it runs the new template, then reloads it. + * + * @return bool Whether the staged files were promoted. + * @throws \Exception + */ +function promote_staged_wildcard_files(): bool { + + $stage = get_wildcard_staging_dir(); + if ( ! is_dir( $stage ) || ! proxy_has_acl_template() ) { + return false; + } + + $ht = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd'; + $vd = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d'; + + foreach ( Site::all() as $site ) { + $url = $site->site_url; + $names = array_filter( get_site_auth_domains( $url, $site ), __NAMESPACE__ . '\is_wildcard_auth_name' ); + if ( empty( $names ) ) { + continue; + } + + $current = true; + foreach ( $names as $name ) { + $current = $current && staged_file_is_current( "$ht/$url", "$stage/htpasswd/$name" ) && staged_file_is_current( "{$vd}/{$url}_acl", "$stage/vhost.d/{$name}_acl" ); + } + + if ( ! $current ) { + // Auth changed since staging, e.g. by the older ee after an interrupted upgrade. + EE::debug( "Staged wildcard auth files of $url are outdated, regenerating them." ); + add_site_auth_files( $url, $site, [] ); + continue; + } + + foreach ( $names as $name ) { + if ( is_file( "$stage/htpasswd/$name" ) ) { + copy_proxy_file( "$stage/htpasswd", $name, [ $name ], $ht ); + } + if ( is_file( "$stage/vhost.d/{$name}_acl" ) ) { + copy_proxy_file( "$stage/vhost.d", $name . '_acl', [ $name . '_acl' ], $vd ); + } + } + } + + // Files of sites deleted since staging are dropped with it. + ( new Filesystem() )->remove( $stage ); + \EE\Site\Utils\reload_global_nginx_proxy(); + EE::debug( 'Promoted the staged wildcard auth files.' ); + + return true; +} diff --git a/src/helper/hooks.php b/src/helper/hooks.php index 4bcc2b6..c28c4c2 100644 --- a/src/helper/hooks.php +++ b/src/helper/hooks.php @@ -10,6 +10,8 @@ use function EE\Auth\Utils\add_site_auth_files; use function EE\Auth\Utils\get_alias_auth_domains; use function EE\Auth\Utils\get_site_auth_domains; +use function EE\Auth\Utils\get_wildcard_staging_dir; +use function EE\Auth\Utils\promote_staged_wildcard_files; use function EE\Auth\Utils\remove_auth_files; use function EE\Auth\Utils\site_auth_files_missing; @@ -115,7 +117,46 @@ function remove_auth_on_alias_domains_update_failure( $site_url, $domains_to_add } } +/** + * Hook to apply the wildcard auth files staged by the auth migration once the new nginx-proxy runs. + */ +function promote_staged_wildcard_auth() { + + if ( ! is_dir( get_wildcard_staging_dir() ) ) { + return; + } + + try { + promote_staged_wildcard_files(); + } catch ( \Throwable $e ) { + // Until promoted, subsites stay unprotected as before the upgrade; retried on the next run. + EE::warning( 'Could not apply the staged wildcard auth files: ' . $e->getMessage() ); + } +} + +/** + * Hook to promote staged wildcard auth files left by an interrupted or failed upgrade, once per run. + */ +function maybe_promote_staged_wildcard_auth() { + + static $checked = false; + + if ( $checked || ! defined( 'EE_PROXY_TYPE' ) || ! is_dir( get_wildcard_staging_dir() ) ) { + return; + } + $checked = true; + + // Not while a migration is pending: its image migration may still bring back the old proxy. + if ( EE_VERSION !== \EE\Model\Option::get( 'version' ) ) { + return; + } + + promote_staged_wildcard_auth(); +} + EE::add_hook( 'site_cleanup', 'cleanup_auth_and_whitelist' ); +EE::add_hook( 'after_docker_image_migration', 'promote_staged_wildcard_auth' ); +EE::add_hook( 'find_command_to_run_pre', 'maybe_promote_staged_wildcard_auth' ); EE::add_hook( 'site_alias_domains_before_update', 'add_auth_before_alias_domains_update' ); EE::add_hook( 'site_alias_domains_updated', 'update_auth_on_alias_domains_change' ); EE::add_hook( 'site_alias_domains_update_failed', 'remove_auth_on_alias_domains_update_failure' ); From af7ab2c18b27c224d7eb060b3fdb984b3f404c2c Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 25 Sep 2026 11:57:18 +0000 Subject: [PATCH 3/3] refactor(migration): stage wildcard auth files in the backup dir The staged _wildcard.* files now live in EE_BACKUP_DIR/auth-wildcard-staging/{htpasswd,vhost.d} (/opt/easyengine/.backup/...) instead of a new /opt/easyengine/.staging dir. It's still outside the proxy's mounts and 0700. Site names always contain a dot, so no per-site .backup// dir can clash with it. The migration, the promotion, the self-heal and down() all use get_wildcard_staging_dir(). --- .../20260924120000_auth-command_regenerate_site_auth_files.php | 2 +- src/auth-utils.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php index 84bb033..6fdb4ed 100644 --- a/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php +++ b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php @@ -49,7 +49,7 @@ public function up() { // The old template applies `_wildcard.X` to every subdomain of X, even on a container event without a reload, so those files wait outside its mounts. if ( ! $new_template ) { - $this->fs->mkdir( [ dirname( $stage ), $stage, "$stage/htpasswd", "$stage/vhost.d" ], 0700 ); + $this->fs->mkdir( [ $stage, "$stage/htpasswd", "$stage/vhost.d" ], 0700 ); } foreach ( $this->sites as $site ) { diff --git a/src/auth-utils.php b/src/auth-utils.php index 9f308ec..4ed908f 100644 --- a/src/auth-utils.php +++ b/src/auth-utils.php @@ -452,7 +452,8 @@ function is_wildcard_auth_name( string $name ): bool { */ function get_wildcard_staging_dir(): string { - return EE_ROOT_DIR . '/.staging/auth-wildcard'; + // Site names always contain a dot, so no per-site `.backup//` dir can clash with it. + return EE_BACKUP_DIR . '/auth-wildcard-staging'; } /**