Selective Elevation of PHP in Nginx+WordPress

For the purpose of this write up, I am demonstrating the configuration as they are on a FreeBSD 9.0 server. The ‘www’ user and group are often referred to as ‘www-data’ on most linux hosts. For saftey Nginx and PHP-FPM are run as www:www, with ownership of the web files belonging to an unprivileged user and permissions set accordingly.

On most of my servers my web files are owned by an unprivileged user that belongs to the www group, and I tend to run nginx and php as www:www. Running PHP as said user:www can be quite risky since most files have read/write/execute permission set on the owner bit. (in octal permissions it’s UGO – User/Owner-Group-Other, such as 750 = owner(read/write/excute) + group (read/execute) + other (none))

In this write up I am simply going to show you the configuration of second a second smaller PHP-FPM pool to act as an ‘elevated’ process running as the folder’s owner, thus allowing far more control over the files and folders such as upgrading wordpress or it’s plugins, but not allow any other files not matching the pattern to do the same.

First thing we need to do is add a second pool to the php-fpm.conf, on FreeBSD installed from ports it will be located at /usr/local/etc/php-fpm.conf, on other systems such as debian or ubuntu, it may be located as /etc/php5/fpm/pool.d/default or similar (look for the config with the [www] pool in it).

This configuration has been stripped down quite a bit with some commented options shown for example purposes.

[global]
pid = run/php-fpm.pid
 
[www]
user = www
group = www
listen = /tmp/php.sock
listen.owner = www
listen.group = www
listen.mode = 0666
 
;listen is not required with sockets
;listen.allowed_clients = 127.0.0.1
 
pm = static 
pm.max_children = 4
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 1024 
 
;chroot = 
;chdir = /var/www
;catch_workers_output = yes
;security.limit_extensions = .php .php3 .php4 .php5
 
;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin
;env[TMP] = /tmp
;env[TMPDIR] = /tmp
;env[TEMP] = /tmp
 
;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
;php_flag[display_errors] = off
;php_admin_value[error_log] = /var/log/fpm-php.www.log
;php_admin_flag[log_errors] = on
;php_admin_value[memory_limit] = 32M
 
[elevated]
user = kbeezie
group = www
listen = /tmp/php-elevated.sock
listen.owner = www
listen.group = www
listen.mode = 0666
;listen.allowed_clients = 127.0.0.1
pm = static 
pm.max_children = 2
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 1024

In the above I have two pools set up, the first one being a typical 4-process (with static, min/max_*servers values are ignored) running as www:www, followed by a second pool named elevated running with 2 processes with the user/group of kbeezie:www (the web files are owned by kbeezie:www).

You’ll notice from some of the commented values above, that you can tweak specific configurations for each pools as needed, such as needing more memory or being locked into a specific folder.

Now in a very simple server block on Nginx for wordpress we need to add a couple location blocks using a regular expression location (this way it overrides the usual php location block when placed ahead of it).

	server { 
		listen 80;
		server_name mywebsite.com;
		root /home/kbeezie/www/mywebsite.com;
 
		location / {
			try_files $uri $uri/ /index.php;
		}
 
		# matches for update.php or update-core.php in /wp-admin/
		location ~ ^/wp-admin/(?:update|update-core)\.php$ {
			include php-elevated.conf;
		}
 
		include php.conf;
		include drop.conf;
	}

I tend to use config files for my PHP inclusion. For example here are both the php.conf and php-elevated.conf located in the same folder as nginx.conf:

location ~ \.php$ {
        try_files $uri =404;
 
        fastcgi_param  QUERY_STRING       $query_string;
        fastcgi_param  REQUEST_METHOD     $request_method;
        fastcgi_param  CONTENT_TYPE       $content_type;
        fastcgi_param  CONTENT_LENGTH     $content_length;
 
        fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
        fastcgi_param  SCRIPT_FILENAME    $request_filename;
        fastcgi_param  REQUEST_URI        $request_uri;
        fastcgi_param  DOCUMENT_URI       $document_uri;
        fastcgi_param  DOCUMENT_ROOT      $document_root;
        fastcgi_param  SERVER_PROTOCOL    $server_protocol;
 
        fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
        fastcgi_param  SERVER_SOFTWARE    nginx;
 
        fastcgi_param  REMOTE_ADDR        $remote_addr;
        fastcgi_param  REMOTE_PORT        $remote_port;
        fastcgi_param  SERVER_ADDR        $server_addr;
        fastcgi_param  SERVER_PORT        $server_port;
        fastcgi_param  SERVER_NAME        $server_name;
 
	fastcgi_pass unix:/tmp/php.sock;
}
location ~ \.php$ {
        try_files $uri =404;
 
        fastcgi_param  QUERY_STRING       $query_string;
        fastcgi_param  REQUEST_METHOD     $request_method;
        fastcgi_param  CONTENT_TYPE       $content_type;
        fastcgi_param  CONTENT_LENGTH     $content_length;
 
        fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
        fastcgi_param  SCRIPT_FILENAME    $request_filename;
        fastcgi_param  REQUEST_URI        $request_uri;
        fastcgi_param  DOCUMENT_URI       $document_uri;
        fastcgi_param  DOCUMENT_ROOT      $document_root;
        fastcgi_param  SERVER_PROTOCOL    $server_protocol;
 
        fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
        fastcgi_param  SERVER_SOFTWARE    nginx;
 
        fastcgi_param  REMOTE_ADDR        $remote_addr;
        fastcgi_param  REMOTE_PORT        $remote_port;
        fastcgi_param  SERVER_ADDR        $server_addr;
        fastcgi_param  SERVER_PORT        $server_port;
        fastcgi_param  SERVER_NAME        $server_name;
 
	fastcgi_pass unix:/tmp/php-elevated.sock;
}

The only difference you’ll notice is that php-elevated.conf connects to the elevated socket rather than the usual one.

Now once we’ve restarted Nginx and PHP-FPM with the new configuration, when you attempt to update wordpress or one of it’s plugins, the script will have an elevated permission due to running as the files/folder’s owner, while any other scripts (as called by the browser) will run with unprivileged www:www status.

The drop.conf is simply a number of locations that should normally be dropped from public requests, I paste it here for reference:

	# the first line can normally be omitted for wordpress, as robots.txt is normally dynamic
	location = /robots.txt  { access_log off; log_not_found off; }
	location = /favicon.ico { access_log off; log_not_found off; }	
	location ~ /\.          { access_log off; log_not_found off; deny all; }
	location ~ ~$           { access_log off; log_not_found off; deny all; }

Cavets

Bear in mind the above configuration are for example purposes, there are quite a few smaller things to keep in mind such as hardening your configuration for more security concerns or optimizing it for performance. For some examples of typical Nginx configurations see : Nginx Configuration Examples

Comments are closed.