nginx + php-fpm

This entry describes installation and configuration of nginx in conjunction with php-fpm on linux.

nginx: installation

You can install it using a package manager or, like me, compile it from source - because OpenSuSE does not offer nginx in their standard repo. The source can be found on their download page. Apart from gcc, you will also need libxslt, gd, openssl and geoip ( +dependencies, and maybe more).

zypper in gcc libxslt-devel gd-devel libGeoIP-devel libopenssl-devel

After unpacking the nginx source code, enter the directory and configure according to your wishes, e.g.

./configure --prefix=/usr \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error-log \
--http-log-path=/var/log/nginx/access-log \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx/nginx.pid \
--user=wwwrun \
--group=www \
--builddir=build1 \
--with-threads \
--with-ipv6 \
--with-file-aio \
--with-http_ssl_module \
--with-http_spdy_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_xslt_module \
--with-http_image_filter_module \
--with-http_geoip_module \
--with-http_sub_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_auth_request_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module

The default settings for configure assume some pretty "interesting" defaults, so it is worth setting all the paths to match the conventions of your distribution. Then just

make
make install

nginx: configuration

A list of configuration file directives can be found here. An old, but still valid and good introduction to nginx can be found here. It is a good introduction for people coming from apache. Also, if you come from apache and have started using rewrites quite extensively, know that nginx has a totally different approach to the whole web-serving business and you should not use nginx' rewrite functionality until you are totally sure you need it.

For my needs, the following is a very working configuration.

worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;

    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  example.net;

        location / {
            fastcgi_param  SCRIPT_FILENAME    /index.php;
            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  REQUEST_URI        $request_uri;
            fastcgi_param  DOCUMENT_URI       $document_uri;
            fastcgi_param  DOCUMENT_ROOT      $document_root;
            fastcgi_param  SERVER_PROTOCOL    $server_protocol;
            fastcgi_param  HTTPS              $https if_not_empty;

            fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
            fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

            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;

            # PHP only, required if PHP was built with --enable-force-cgi-redirect
            fastcgi_param  REDIRECT_STATUS    200;

            fastcgi_pass 127.0.0.1:9000;
        }
    }
}

You might want to adjust worker_processes to your needs. This configuration file basically just directs any request to example.net to a cgi server listening on 127.0.0.1:9000, at the file index.php. That means that whatever is entered into the url bar after http://example.net/ does not matter, everything goes to index.php. If the application is written properly, it will parse the $_SERVER[REQUEST_URI] and act accordingly. For example, if the request is http://example.net/a/b/c?x=y, then index.php will be run and $_SERVER[REQUEST_URI]="/a/b/c?x=y".

This way, it is of course totally impossible to access any other files that the cgi server might serve. If you need to access other files directly (i.e. not going via index.php), maybe you need images or a css file, there are simple solutions for this, e.g.

location / {
    root /srv/www/pools/example.net
    try_files   $uri $uri/ /index.php;
}

This redirects everything that does not exist to /index.php. However, any attacker that manages to place a php file into your cgi directory can then execute it. In my original solution, everything gets redirected to /index.php, so it is totally impossible to directly call any other scripts or executables. Also, since everything goes via php-fpm, no root directive is needed.

Another secure method would be to put all static content into separate directories from executable (read: scripts) content and filter those directories via a separate location directive. The location directive is really important, the documentation can be found here.

php-fpm

Installation was done via the package manager in this case (thankfully, php-fpm is included in the standard OpenSuSE repos). A config file template is stored at /etc/php5/fpm/php-fpm.conf.default. You can copy this to /etc/php5/fpm/php-fpm.conf or use the one below:

[global]
error_log = /var/log/php-fpm/errors.log
log_level = notice
process.max = 128
daemonize = yes
systemd_interval = 10

include=/etc/php5/fpm/pool.d/*.conf

This is very simple. More configuration directives (although not all) can be found in the official documentation. Some extra ones are found in php-fpm.conf.default. Note that this configuration also uses some other *inis to set php behaviour. In my case (checked via phpinfo()) these were

Loaded Configuration File /etc/php5/fpm/php.ini
Additional .ini files parsed: /etc/php5/conf.d/ctype.ini, /etc/php5/conf.d/dom.ini, /etc/php5/conf.d/iconv.ini, /etc/php5/conf.d/json.ini, /etc/php5/conf.d/pdo.ini, /etc/php5/conf.d/pdo_sqlite.ini, /etc/php5/conf.d/sqlite3.ini, /etc/php5/conf.d/tokenizer.ini, /etc/php5/conf.d/xmlreader.ini, /etc/php5/conf.d/xmlwriter.ini

To load sensible defaults it is probably a good idea to copy the /etc/php5/cli/php.ini to /etc/php5/fpm/php.ini (as I did). Otherwise, no php.ini is loaded except for those in the conf.d directory. You might also want to tweak the php.ini to your needs (e.g. regarding file uploads, max post size etc.). However, any more specific settings should be done per-pool.

An example of a pool configuration file (/etc/php5/fpm/pool.d/example.net.conf) is shown below:

[example.net]
listen = 127.0.0.1:9000
listen.allowed_clients = 127.0.0.1
prefix = /srv/www/pools/$pool
chroot = $prefix
user = phpfpm
group = phpfpm
pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 2
pm.max_requests = 500
; pm.status_path = /status
; ping.path = /ping
; ping.response = pong
request_terminate_timeout = 120s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/$pool.log.slow
access.log = /var/log/php-fpm/$pool.access.log
access.format = "%R - %u %t \"%m %l %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
clear_env = Yes
security.limit_extensions = .php

You can add per-pool php settings for example like this:

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

More about this can be found in the documentation (search for "update PHP settings of a certain pool" on the page).

systemd service scripts

OpenSuSE provides a (crappy) php-fpm systemd service script at /usr/lib/systemd/system/php-fpm.service. The original one is pretty "bad" because it runs php-fpm in foreground and also does not check the config file for errors before starting the service. I have corrected this, my version can be found below:

[Unit]
Description=The PHP FastCGI Process Manager
After=syslog.target network.target
Before=apache2.service nginx.service lighttpd.service

[Service]
Type=forking
PIDFile=/var/run/php-fpm/php-fpm.pid
ExecStartPre=/usr/sbin/php-fpm --fpm-config /etc/php5/fpm/php-fpm.conf -t
ExecStart=/usr/sbin/php-fpm --fpm-config /etc/php5/fpm/php-fpm.conf --pid /var/run/php-fpm/php-fpm.pid
ExecReload=/bin/kill -USR2 $MAINPID
ExecStop=/bin/kill -QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Since I did not install nginx via the package manager, there is no systemd service file (it was also not installed as part of the installation process). You should add one yourself at /usr/lib/systemd/system/nginx.service:

[Unit]
Description=The nginx HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/nginx/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/usr/sbin/nginx -s quit
PrivateTmp=true

[Install]
WantedBy=multi-user.target

logrotate

All of this creates quite a few log files, and none of them are automagically rotated. For this purpose, add two new files in /etc/logrotate.d/: nginx and php-fpm, shown below:

/var/log/nginx/*-log {
    delaycompress
    compress
    dateext
    maxage 365
    rotate 99
    size=+4096k
    notifempty
    missingok
    create 644 root root
    sharedscripts
    postrotate
     /usr/sbin/nginx -s reopen
    endscript
}
/var/log/php-fpm/*.log /var/log/php-fpm/*.slow {
    delaycompress
    compress
    dateext
    maxage 365
    rotate 99
    size=+4096k
    notifempty
    missingok
    create 644 root root
    sharedscripts
    postrotate
     [ ! -f /var/run/php-fpm/php-fpm.pid ] || kill -USR1 `cat /var/run/php-fpm/php-fpm.pid`
    endscript
}

You can test them using logrotate -d -f *configfile*. If this gives a ton of output, copy the /etc/logrotate.conf to a temporary directory and change it to only include /etc/logrotate.d/php-fpm and /etc/logrotate.d/nginx.

final stuff

You should check if all required directories were created and create them if necessary. They can all be owned by root. Both nginx and php-fpm are started as root and have their master processes run as root, while the workers run with the configured user. Therefore, nginx and php-fpm can create logfiles in locations where their workers can not. Some directories to check:

  • /var/run/php-fpm/
  • /var/run/nginx/
  • /var/log/php-fpm/
  • /var/log/nginx/
  • /srv/www/pools/*name of pool*

Furthermore, the user and group we configured php-fpm to use needs to be created. wwwrun and www for nginx should already exist.

useradd -U -s /bin/false phpfpm

Please do *not* change the ownership of anything to the phpfpm user. php does not need write permissions on a script to be able to execute it, nor does it need ownership of it or of the parent directory. Both possibly enable attackers to modify server contents. If an application needs write permissions (e.g. for file uploads), they mostly need it on a particular directory only. If an application requires write permissions to everything, do not use it, it is crap. You might as well give your server to spammers for free.

 

This concludes this setup. If you find any bugs or problems with this configuration, please send me an email.

© 2010-2015 Stefan Birgmeier
birki@21er.org