why unix | RBL service | netrs | please | ripcalc | linescroll | pwtool
hosted services

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.