WordPress: scheme in wordpress__url, hardening, read-only core, and a setup_wordpress scenario - #382
Open
NavidSassan wants to merge 19 commits into
Open
NavidSassan wants to merge 19 commits into
NavidSassan wants to merge 19 commits into
Conversation
wp_set_auth_cookie() marks the logged_in cookie Secure only if home is an https URL (wp-includes/pluggable.php), and wp core install --url=<host> stored http://, so that cookie never got Secure. wordpress__url now carries the scheme and is the single source for the site address: WordPress is installed with it, home and siteurl are set to it on every run, and FORCE_SSL_ADMIN / FORCE_SSL_LOGIN and the cron request's X-Forwarded-Proto follow it. The host part is derived with regex_replace instead of urlsplit, so that a bare host from an old inventory still yields the same vHost and installation directory in apache_httpd and selinux, which setup_wordpress runs before the role's assert aborts.
… vHost context In vHost context the URL path starts with a slash, so the rules quoted from the .htaccess hardening guide never matched and wp-includes/*.php could be called directly. Verified with httpd 2.4.62 on Rocky 9.
An upload flaw in any plugin turned into code execution, because the PHP-FPM handler applied below uploads as well. Verified with httpd 2.4.62 and php-fpm 8.0 on Rocky 9, including .PHP, .phtml and //.
…ordPress vHost "Header always set", because PHP-FPM responses carry their headers in the table only "always" edits; WordPress sends both itself on some admin requests, which would duplicate them otherwise.
…sers The recurring chmod 644 left the database password and the salts readable for every local user. wp config set and blockinfile keep the 0640 (verified with WP-CLI 2.12).
…sted_proxies Any client could set REMOTE_ADDR and HTTP_HOST. X-Forwarded-For is now walked from the right on requests from a listed proxy and validated as an IP; X-Forwarded-Host is dropped, since the vHost only accepts the ServerName as Host anyway. X-Forwarded-Proto stays unrestricted, a client faking it only changes what it is served itself.
WordPress answers a failed login with the form again (200) and a successful one with a redirect (302). Verified with fail2ban-regex 1.1.0 on Rocky 9 against combined, linuxfabrikio and vhost_common lines.
wp-cli publishes wp-cli.phar.sha512 next to the phar; get_url replaces a differing file and aborts on a mismatch.
--dbpass and --admin_password were readable in the process list and appeared in the task names. WP-CLI's --prompt reads them from stdin, and --quiet keeps them out of the output, because WP-CLI prints the completed command line, password included, after prompting (verified with WP-CLI 2.12).
/tmp left the export, with unpublished content and the users' e-mail addresses, readable for other local users.
WordPress has no wp-config.php constant for this, so the role deploys a must-use plugin that filters wp_is_application_passwords_available (verified against WordPress in a container).
…rdPress The tasks read the path from a loop variable without a loop, so the condition was always false and the import never ran.
The WP-CLI tasks set become_user without become, so with a root
connection they ran as root and WP-CLI refused ("YIKES! It looks like
you're running this as root"). They now become apache like the
nextcloud role does.
`wp config set` reports "Updated" even for an unchanged value, so the
constants are read with `wp config get --format=json` first and only
set when they differ. Plugin and theme installs report a change only
when WP-CLI installs or activates something. Verified with WP-CLI 2.12.
verify.yml asserts through Apache httpd and PHP-FPM: the address from wordpress__url, the configured plugins and theme, a login with a Secure cookie and the HTTPS redirect, the security headers, the referer, wp-includes and upload rules of the vHost, X-Forwarded-For trust, application passwords and the mode of wp-config.php. Passes converge, verify and idempotence on Rocky 8, 9 and 10.
…il2ban scenario The jail needs Apache logs the scenario host does not have, so the deployed filter is fed sample lines through fail2ban-regex. Passes on Rocky 8, 9 and 10 (fail2ban 1.1.0).
The setup_wordpress scenario passes converge, verify and idempotence on Rocky 8, 9 and 10.
NavidSassan
force-pushed
the
feat/wordpress-url-scheme
branch
from
September 15, 2026 13:05
36a28c5 to
2fe7c6e
Compare
… API Disable REST API (disable-json-api 1.8, last updated 2023-09-14) stores the routes that exist when its settings are created as blocked for anonymous visitors and falls back to its global default_allow (true) for every other route. Routes that plugins installed later register therefore stayed reachable without a login: in a container, /wp/v2/users answered 401 while such a route answered 200. Disable WP REST API (2.6.9, maintained) returns rest_login_required from rest_authentication_errors for every anonymous REST request, which WordPress evaluates before dispatching any route. The role uninstalls the old plugin. setup_wordpress passes on Rocky 9 and asserts the 401.
The core, wp-config.php and wp-content/mu-plugins belong to root, the rest of wp-content to apache, so a vulnerable plugin can no longer plant code in the core. FS_METHOD direct keeps plugin and theme installs from the web interface working, since WordPress otherwise asks for FTP credentials when its files belong to another user. WP-CLI runs as root only for commands annotated `@when before_wp_load` (core download, config, core verify-checksums). Everything that loads WordPress runs as apache, because loading it executes wp-content code (plugins, mu-plugins, drop-ins) even with --skip-plugins. WP_AUTO_UPDATE_CORE is false; wordpress-core-minor-update.timer installs the latest minor release instead. setup_wordpress skips the document root chown of apache_httpd. Verified with the setup_wordpress scenario on Rocky 8, 9 and 10.
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.
The
wordpressrole and its vHost get a round of security fixes, the WordPress core becomes read-only for the web server,wordpress__urlcarries the scheme, and a newsetup_wordpressMolecule scenario covers the whole stack. Running it on Rocky 8, 9 and 10 surfaced two bugs outside the role, which are already merged intomain.What is in here
WordPress (breaking)
wordpress__urlincludes the scheme and is the single source for the site address (home/siteurl, set on every run). The login cookie only getsSecurewith anhttpshome, and sites installed by the role hadhttp://.FORCE_SSL_ADMINand the cron request follow the scheme.X-Forwarded-Foris only honoured on requests fromwordpress__trusted_proxies, read from the right.X-Forwarded-Hostis ignored.wordpress__application_passwords_enabled), set through a must-use plugin.wp-config.phpandwp-content/mu-pluginsbelong toroot; onlywp-contentbelongs toapache(FS_METHODdirect, so plugins and themes still install from the web interface). WP-CLI runs asrootonly for commands that do not load WordPress, everything else asapache. WordPress no longer updates its core:wordpress-core-minor-update.timerinstalls minor releases,--tags wordpress:updatethe rest.setup_wordpressskips the document root chown ofapache_httpd.disable-json-api, which left routes of later installed plugins open, and the role uninstalls that plugin.WordPress and its vHost
wp-content/uploads.wp-includesblock matches in vHost context; it never did before.Referrer-Policyandnosniff.wp-config.phpisroot:apache0640.--prompton stdin./backup/wordpress-export.remote_tmppath,becomefor WP-CLI, and idempotence of the WP-CLI tasks.fail2ban: a
wordpress-loginfilter andz10-wordpress-loginjail (failed login = POSTwp-login.phpanswered 200).Found by the scenario, merged separately into
main: the apache_httpd document-root chown that broke idempotence on fresh hosts, and the missing SELinux label for/usr/bin/mariadbdon RHEL 10.Points worth a close look
wordpress__url,wordpress__trusted_proxiesbehind a proxy, andwordpress__application_passwords_enabled: truewhere integrations need it..htaccess: after a permalink change, the displayed rules go into.htaccessby hand.regex_replacefor the host part is intentional:setup_wordpressrunsapache_httpdandselinuxbefore the role's assert. A comment indefaults/main.ymlexplains it.Testing
setup_wordpressandfail2ban/installpass converge, verify, idempotence and verify on Rocky 8, 9 and 10;apache_httpdon Rocky 9.setup_wordpress/verify.ymlasserts through Apache and PHP-FPM:Securecookie, and the HTTPS redirectwp-includesand upload rulesX-Forwarded-Fortrustwp-config.php401 rest_login_requiredapachecan writewp-contentbut not the core,wp-config.phpormu-plugins, andget_filesystem_method()isdirectwordpress-core-minor-update.serviceruns, and its timer is enabled