hosted services
Recently needed one part of a site to run as one user, and another path to run as another. I needed WordPress to run as a non-content owner and yet allow wp-admin to be able to change bits of the site.
There's two main ways I see to do this, either use AssignUserID* or
tell Apache to proxy the traffic to one of two PHP FPMs. This is
probably better, since Apache doesn't have to fork a new child and
setuid. The first part of this article talks about using
AssignUserIDExpr, you'll find the proxy method at the end.
assignuserid
In this setup, dummy is the owner of the WP content directories, dummy-read doesn't have any write permissions within the WP structure.
Here's what I did:
RewriteEngine on
RewriteRule ^/wordpress.* - [E=ITK:dummy_read]
RewriteRule ^/wordpress/wp-admin.* - [E=ITK:dummy]
AssignUserIDExpr %{reqenv:ITK}
This needs mod_itk (AKA: libapache2-mpm-itk) and mod_rewrite of course.
Other ways could be to setup multiple FPM servers with different users
to run as, then rewrite to each as a proxy to the socket depending on
the conditions. Up to you!
RewriteRule ^ - [E=ITKUID:dummy_read,E=DB_USER:mysite_read,E=DB_PASSWORD:mysite,E=DB_NAME:mysite,E=DB_HOST:localhost]
RewriteCond %{QUERY_STRING} ^.*rest_route.*
RewriteCond %{REQUEST_METHOD} POST
RewriteRule ^/wordpress/index.php.* - [E=DB_USER:mysite,E=DB_PASSWORD:mysite,E=DB_NAME:mysite,E=DB_HOST:localhost]
RewriteRule ^/wordpress/(wp-admin|wp-login) - [E=ITKUID:dummy,E=DB_USER:mysite,E=DB_PASSWORD:mysite,E=DB_NAME:mysite,E=DB_HOST:localhost]
AssignUserIDExpr %{reqenv:ITKUID}
sql (optional)
For some sites a rewrite rule setup with the following pattern permits
only some requests through using restricted access. I created a reader
user with just select and the other paths get read/write:
--- wp-config-sample.php 2024-03-11 14:08:10.000000000 +0000
+++ wp-config.php 2024-12-01 13:38:39.852873921 +0000
@@ -20,16 +20,16 @@
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
-define( 'DB_NAME', 'database_name_here' );
+define( 'DB_NAME', getenv('DB_NAME') ? getenv('DB_NAME') : 'mysite');
/** Database username */
-define( 'DB_USER', 'username_here' );
+define( 'DB_USER', getenv('DB_USER') ? getenv('DB_USER') : 'mysite_read' );
/** Database password */
-define( 'DB_PASSWORD', 'password_here' );
+define( 'DB_PASSWORD', getenv('DB_PASSWORD') ? getenv('DB_PASSWORD') : 'mysite');
/** Database hostname */
-define( 'DB_HOST', 'localhost' );
+define( 'DB_HOST', getenv('DB_HOST') ? getenv('DB_HOST') : 'localhost');
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8' );
In mysql:
MariaDB [(none)]> grant all on mysite.* to mysite@'%' identified by 'mysite';
MariaDB [(none)]> grant select on mysite.* to mysite_read@'%' identified by 'mysite';
What this does above is set the DB based on the environment variables. The most important is DB_NAME, if you don't want to put the password in the environment then you can set it in wp-config.php like normal, just get the DB_USER from environment.
locationmatch
This works using LocationMatch with ProxyBalancer too, if that fits
your setup, some advantages of proxy pass to FPM allows you to run
Apache in a non-preforking model (doesn't have to fork() for all
requests and set the process owner ID).
It does have to IPC with the FPM though, but this is likely a very minor overhead compared to serving every single (non-PHP too) request in a forked model.
RewriteCond %{QUERY_STRING} ^.*rest_route.*
RewriteCond %{REQUEST_METHOD} POST
RewriteRule ^/wordpress/index.ph.* balancer://writer [P]
<LocationMatch "^/wordpress/.*php">
ProxyPass "balancer://reader"
ProxyPassReverse "balancer://reader"
</LocationMatch>
<LocationMatch "^/wordpress/(wp-admin|wp-login).*php">
ProxyPass "balancer://writer"
ProxyPassReverse "balancer://writer"
</LocationMatch>
<Proxy "balancer://reader">
SetEnv DB_USER mysite_read
SetEnv DB_PASSWORD mysite
SetEnv DB_NAME mysite
SetEnv DB_HOST localhost
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "%{DOCUMENT_ROOT}/%{REQUEST_URI}"
BalancerMember "unix:/var/run/php/php8.2-fpm-dummy_read.sock|fcgi://localhost"
</Proxy>
<Proxy "balancer://writer">
SetEnv DB_USER mysite
SetEnv DB_PASSWORD mysite
SetEnv DB_NAME mysite
SetEnv DB_HOST localhost
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "%{DOCUMENT_ROOT}/%{REQUEST_URI}"
BalancerMember "unix:/var/run/php/php8.2-fpm-dummy.sock|fcgi://localhost"
</Proxy>
This might be more convenient if you don't like using
AssignUserIDExpr.