feat(auth): support http auth and ip whitelist on wildcard subdomains and alias domains - #57
Merged
Merged
Conversation
- Modified generate_site_auth_files() to create _wildcard.site_url file for subdomain multisites (app_sub_type === 'subdom') - Handle alias_domains: create htpasswd files for each alias domain - Convert *.domain format aliases to _wildcard.domain - For subdomain multisites, also create wildcard versions of alias domains - Updated all callers to pass site_data parameter - Fetch site data from DB when regenerating auth for all sites globally Signed-off-by: Riddhesh Sanghvi <riddhesh237@gmail.com>
Add array_unique() to deduplicate domains array before processing, preventing duplicate htpasswd file creation when alias domains overlap with wildcard domains (e.g., *.example.com alias on a subdomain multisite) Signed-off-by: Riddhesh Sanghvi <riddhesh237@gmail.com>
- Modified generate_site_whitelist() to create whitelist files for _wildcard.site_url on subdomain multisites - Handle alias_domains: create whitelist files for each alias domain - Convert *.domain format aliases to _wildcard.domain - For subdomain multisites, also create wildcard versions of alias domains - Updated all callers to pass site_data parameter - Fetch site data from DB when regenerating whitelist for all sites globally Signed-off-by: Riddhesh Sanghvi <riddhesh237@gmail.com>
When alias_domains contains *.site_url (e.g., *.example.com for site example.com), skip it ONLY if the site is a subdomain multisite since _wildcard.site_url is already added in that case. For non-multisite sites with *.site_url alias, create the _wildcard.site_url file. Signed-off-by: Riddhesh Sanghvi <riddhesh237@gmail.com>
When no auths or whitelists exist for a site, explicitly remove all related htpasswd/whitelist files (including wildcards and aliases) and return early. This makes the cleanup behavior more explicit rather than silently iterating over an empty array. Signed-off-by: Riddhesh Sanghvi <riddhesh237@gmail.com>
Always remove _wildcard.site_url htpasswd and whitelist files at the start of regeneration. This ensures orphan wildcard files are cleaned up when a site changes from subdomain multisite to regular site type. Signed-off-by: Riddhesh Sanghvi <riddhesh237@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR adds support for wildcard htpasswd and IP whitelist files for WordPress multisite subdomain installations and alias domains. The main purpose is to enable HTTP authentication and IP whitelisting to work correctly on subdomains of multisite installations (e.g., site1.example.com, site2.example.com) by creating _wildcard.example.com configuration files.
Key changes:
- Modified
generate_site_auth_filesandgenerate_site_whitelistmethods to accept optional$site_dataparameter - Added logic to create wildcard (
_wildcard.domain) htpasswd/whitelist files for subdomain multisites - Added support for processing alias domains, including converting
*.domainformat to_wildcard.domainfilenames - Updated all call sites to pass the
$site_dataparameter
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Auth and whitelist generation duplicated the same domain collection logic. Move it into get_site_auth_domains() and move site-level htpasswd/ACL generation into auth-utils so other callers (hooks) can reuse it. No behaviour change.
A plain alias of a subdomain multisite is never served as *.<alias>, so _wildcard.<alias> protected nothing and could leak onto unrelated hosts.
The unconditional removal of _wildcard.<site> could delete a file generated for another site's *.<site> alias. A site's type can't change from subdomain multisite to a regular site, so the cleanup isn't needed.
Whether to remove a site's htpasswd/ACL files was decided from the merged global and site entries. Since a global whitelist always exists (and global auth may too), site files were never removed and kept overriding the global default files. Decide from the site's own entries instead.
The htpasswd command was built by interpolating the file name, username and password into a shell command. Build it in one helper that shell-escapes every argument and use it for all htpasswd writes.
The site_cleanup hook in src/helper/hooks.php was never loaded because the file wasn't in the autoload files, so site deletion only removed htpasswd/<site> (via site-command) and left _wildcard.<site>, alias htpasswd files and every _acl file behind. These orphans then protected later, unrelated sites with the same names. Load the hooks file and remove the htpasswd and ACL files of every domain the site uses, whether or not it still has auth entries.
Adding an alias with `ee site update --add-alias-domains` didn't generate its htpasswd/ACL files, so the new alias was served without the site's auth until an `ee auth` command was re-run. Removing an alias left its files behind. Listen to site-command's site_alias_domains_updated hook: remove the files of removed aliases, regenerate the site's htpasswd and whitelist files when the site has its own entries, and reload the proxy. Requires a site-command release that fires the hook.
Alias names now have to be a plain hostname or `*.hostname` before they are turned into htpasswd/ACL file names; empty names, `default`, `default_admin_tools` and anything path-like are skipped. An empty alias used to make remove_auth_files() pass the htpasswd directory itself to Filesystem::remove(), wiping every auth file, and `default` deleted the global auth and ACL files. Removal now only deletes a regular file directly inside the htpasswd or vhost.d directory, never touches the global files, and reports whether anything was removed.
…r domains generate_site_auth_files() ran one `docker exec htpasswd` per user and per domain. It now writes the file of the site itself in the proxy container and copies it on the host to the `_wildcard.<site>` and alias files, keeping the source's mode so the result doesn't depend on the umask. If writing fails the existing files are left in place with a warning instead of being removed upfront, so a failed exec can no longer leave the site unprotected.
EE::exec() logs every command in debug mode, so each `htpasswd -b` call wrote the plain password and username to ee.log. write_htpasswd_file() now passes them as obfuscated values, and the admin-tools auth is written through it too.
The htpasswd and ACL files of new alias domains were written from `site_alias_domains_updated`, after docker-compose had already brought up the new VIRTUAL_HOST, so a protected site answered 200 without credentials on the new alias for a few seconds. The new `site_alias_domains_before_update` hook now copies the site's htpasswd file and writes its ACL file for the added aliases before the containers are recreated, but only for the files the site has own entries for. If the update is aborted, `site_alias_domains_update_failed` removes those files again, keeping every file the site still uses. The post-update hook still removes the files of deleted aliases once they are no longer served, and regenerates the files from the saved site only when aliases were added.
…emoved The `site_cleanup` hook reloaded the global proxy on every site delete, also for sites that never had auth or whitelist entries or files. It now reloads only when it deleted a row or a file.
…bal files Site::find() uses the `site_url` primary key and returns false on a miss, which is mapped to null as before.
Installs upgraded from a version that only wrote `htpasswd/<site>` and `<site>_acl` would keep subdomains (`_wildcard.<site>`) and alias domains unprotected until an `ee auth` command touched the site, and kept per-site files of sites without own entries. A container migration now regenerates the htpasswd and ACL files of every site, which also removes the files of sites without own entries, and then reloads the proxy once. A failing site is logged and skipped instead of failing the migration. It is skipped on fresh installs and when there are no sites, and has nothing to revert since the files are also valid for the previous version.
`default` and `default_admin_tools` were listed separately in the alias domain check, the file removal guard and add_site_auth_files(). They now come from RESERVED_AUTH_FILE_NAMES through is_reserved_auth_file_name(), which also matches them in any case, as the alias domain check in site-command does.
is_valid_alias_domain() only allowed letters, digits and `-` in labels, so an alias domain like `my_blog.example.com` got no htpasswd or ACL file and was served without the site's auth. Labels may now also contain `_`, matching the alias domain names site-command accepts.
htpasswd rewrote the live file entry by entry (`-c` for the first, then one call per user), so the proxy could read a file with only some of the users, and a failure midway left it that way. The entries are now written to `.<name>.tmp` in the htpasswd directory, a name no host can match, and the file is renamed over the real one once all entries are in; the site's other domains are then copied from it as before. On failure the temp file is removed, the existing file is left unchanged, and write_htpasswd_file() warns, so the callers no longer do.
htpasswd prints `Adding password for user <name>` on STDERR, and EE::exec() logs the output of every command, so each write still put the usernames in ee.log even though the command itself was masked. The htpasswd output is now discarded; failures are still detected through the exit code, which write_htpasswd_file() checks.
… site's generators add_site_auth_files() reimplemented the htpasswd and ACL generation for the alias domains about to be added. generate_site_auth_files(), generate_site_whitelist() and get_site_auth_domains() now take the alias domains not saved on the site yet, and add_site_auth_files() just calls the two generators with them. It no longer returns whether anything was written, which no caller used. Stale files for the new names are now also removed when the site has no own entries, so they fall back to the global files. The post-update hook no longer regenerates everything (running htpasswd again) whenever alias domains were added: it only rewrites the files if one the site's entries call for is missing, and reloads the proxy only when it changed something.
…when it can't be rewritten generate_site_auth_files() gave up when htpasswd could not run, e.g. when nginx-proxy is stopped while the upgrade migration runs. The migration was then recorded as done, but the subdomain and alias domain files it exists to create were never written, leaving those hosts on the global auth. If the site's own `htpasswd/<site>` exists, it is now copied host-side to the site's other domains, as the alias domain hook did before, so they are at least as protected as the site itself.
The down() comment claimed the regenerated files are also valid for the previous version. With the previous nginx-proxy template a `_wildcard.X` file also protects sibling hosts that share the last labels (e.g. shop.example.com picks up _wildcard.example.com), so the files written here are only correct with the nginx-proxy image shipped in the same release.
This was referenced Sep 24, 2026
Merged
Merged
Alias domains are now checked with site-command's `is_valid_alias_domain()` and the global file names with its `is_reserved_proxy_file_name()`, instead of a separate copy of the rule that disagreed with it (it accepted `-a.com` and `a-.com`). This also skips aliases whose labels start with `_`, so an alias like `_wildcard.example.com` can no longer delete or overwrite the `*.example.com` files of site example.com.
…tten Regenerating global auth removed `htpasswd/default` and `default_admin_tools` before writing the new file, so a failed write left every host that falls back to global auth unprotected. `default` is now only removed when there are no global auths and otherwise replaced atomically, and `default_admin_tools` is removed only once `default` is written. The global rows are also no longer regenerated a second time as if they were sites.
Each copy is written under a name no host matches and then renamed, so the proxy never reads a truncated file. A failed copy is cleaned up and leaves the existing file unchanged.
mrrobot47
added a commit
to EasyEngine/site-command
that referenced
this pull request
Sep 25, 2026
site-command no longer removes `htpasswd/<site>` or the site's auth and whitelist rows in delete_site(). auth-command's `site_cleanup` hook, which runs just before, removes them along with the `_wildcard.<site>`, alias and `_acl` files, so the copy here was a duplicate. Requires auth-command with the site_cleanup hook autoloaded (EasyEngine/auth-command#57). The two must ship in the same core release, or site delete leaves the site's auth files and rows behind.
mrrobot47
added a commit
to mrrobot47/auth-command
that referenced
this pull request
Sep 25, 2026
EasyEngine#57 is on develop now: AUTH-11 (site delete removes the _acl file) and AUTH-12 (shell metacharacters in passwords) pass, so they become regular regression scenarios, and a site without own users or IPs no longer keeps an htpasswd/_acl file (5f12c49), so the two scenarios that asserted the old files check that the files are gone.
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HTTP auth and IP whitelists only covered a site's own domain:
htpasswd/<site>and<site>_aclwere the only files written, so subdomains of a subdomain multisite and alias domains were served with the global auth or none at all. auth-command now writes the files for every domain a site is served on, keeps them in sync when alias domains change, removes all of them on site delete, and regenerates them on upgrade.Changes
_wildcard.<site>for a subdomain multisite, each alias domain, and_wildcard.Xfor a*.Xalias. Plain alias domains get no_wildcardfile.defaultfiles.src/helper/hooks.phpfrom the Composer autoload files. Itssite_cleanuphook was never loaded before; it now removes the auth and whitelist rows and the htpasswd and ACL files of every domain the site uses on site delete, and reloads the proxy only when something was removed.site_alias_domains_before_updatewrites the files for added alias domains before the proxy serves them,site_alias_domains_update_failedremoves them again if the update is aborted, andsite_alias_domains_updatedremoves the files of deleted alias domains.is_valid_alias_domain()rejects (anything but a plain hostname or*.hostname, labels starting with_such as_wildcard.X, and the reserveddefault/default_admin_toolsnames) before turning them into file names; file removal only deletes regular files directly inside the htpasswd or vhost.d directory and never the global files.docker exec htpasswdper user and domain. A failed write leaves the existing files unchanged, including the globaldefaultfile.ee.log.20260924120000_auth-command_regenerate_site_auth_filesregenerates every site's htpasswd and ACL files on upgrade (removing the files of sites without their own entries) and reloads the proxy once. A failing site is logged and skipped. It is skipped on fresh installs and when there are no sites, and itsdown()is empty.Release / merge order
These PRs ship together in one core release:
easyengine/nginx-proxytemplate (needs a new dockerfiles tag)Order:
easyengine/nginx-proxyimage is on Docker Hub.dev-developof every package) and tag each package.easyengine/nginx-proxyinimg-versions.jsontogether.Why they are coupled:
_wildcard.Xfile to sibling sites too (e.g.shop.example.compicks up_wildcard.example.com).site_alias_domains_before_update,site_alias_domains_updatedandsite_alias_domains_update_failedhooks, which only the new site-command fires.site_cleanuphook (autoloaded since feat(auth): support http auth and ip whitelist on wildcard subdomains and alias domains #57) does it.split_alias_domains()andvalidate_alias_domains(), and auth-command its newis_valid_alias_domain()andis_reserved_proxy_file_name(); with an older site-command, they fail with a fatal "undefined function" error._wildcard.Xfiles that were also relied on to protectXitself:Xnow needs its ownhtpasswd/X(andvhost.d/X_acl) file.Testing
*.aliases, multiple users, global auth, IP whitelists, alias add/remove, site delete, self-signed SSL and Let's Encrypt.