Sunday, December 9, 2012

Web served, part 3: Bolting on PHP with PHP-FPM

Web served

  • Web served, part 2: Securing things with SSL/TLS
  • How to set up a safe and secure Web server
View all…

A Web server that can only serve out static pages is fine for a lot of folks. If you just want a homepage with a list of your favorite links and some pictures of your cat, then a bare Web server is all you need. However, if you want to learn about doing more interesting stuff—setting up a forum or a wiki, or using popular blogging apps—then you need some way of generating dynamic content—that is, a website that can be changed or updated programmatically, rather than one made simple static files.

As with most Web server-related things, there are many paths to dynamic content. However, some of the most popular Web applications—things like phpBB, MediaWiki, WordPress, and Drupal—use a server-side scripting language called PHP. That's what we're going to install, because it's relatively easy to get PHP up and running and because having PHP available gives you a tremendous amount of flexibility in what you can do with your Web server.

PHP-FPM

One advantage Apache has over Nginx is the ease with which PHP can be enabled. Nginx, unlike Apache, has no ready-made modules to install, so there are several packages we need to pull down and several configuration files to edit to get PHP working. Never fear, though—we'll cover every detail.

We're going to install a particular PHP bundle called PHP-FPM, where the "FPM" part stands for "FastCGI Process Manager." There's an entire article's worth of discussion on the hows and whys of choosing PHP-FPM, but briefly, we're using this particular PHP package because it includes the ability to scale up (or down) the number of PHP processes that are running and handling requests based on load and because it's fast and it integrates well with Nginx.

Installation and extras

We're going to use the version of PHP-FPM available in the main Ubuntu repositories, which as of this article's publication is 5.3.10. Newer versions of PHP are available (the latest stable version is 5.4.9), but not all applications have been fully modified to work with PHP 5.4 yet, so we're sticking with the default for greater compatibility.

Verify that your repository listings are up to date with a quick sudo aptitude update, and then run the following commands to install PHP-FPM and the add-on modules we want:

 sudo aptitude install php5-fpm php5-suhosin php5-gd php-apc php5-mcrypt php5-cli php5-curl 

(Depending on what's installed, you may be prompted to remove a package named libgd2-noxpm before the installation can continue; if so, go ahead and allow it to be removed. This is a graphics library used by PHP, and it will be replaced with an alternate version during installation.)

Here's what each of those packages does:

  • php5-fpm: The base PHP-FPM package.
  • php5-suhosin: The Suhosin PHP security patch, which helps to make PHP more secure.
  • php5-gd: A PHP module that allows graphics to be drawn directly by PHP scrips. Required for some applications.
  • php-apc: The PHP Alternative Cache, a module that can drastically improve PHP's speed by caching compiled PHP scripts rather than making the server run the same scripts over and over again.
  • php5-mcrypt: A module which gives PHP access to a wide range of encryption and decryption functions. Required for some applications.
  • php5-cli: Gives you the ability to run PHP scripts from the command line. Useful for troubleshooting and because some applications, like MediaWiki, require you to manually execute scripts to do updates or other administrative functions.
  • php5-curl: Gives PHP the ability to use cURL. Required for some applications.

One more thing to install

We want to install one more thing: memcached (that's pronounced "memcache dee," as in the memcache daemon, not "memcached" as in past tense). This application is a key-value store used by many big websites for caching. We want it for a very specific purpose, which we'll get to near the end of our configuration process. For now, just install memcached and its PHP module, and we'll come back to it in a bit:

 sudo aptitude install memcached php5-memcache 

Where is all this stuff?

Once all this is installed, you'll have a new directory called /etc/php5 with several subdirectories underneath:

Enlarge / The PHP directory. (This is not a photograph of my screen. This is Cathode.)

All of the PHP-related configuration files we need to edit are in here. There are several, but fortunately we don't need to make too many changes in order to get things set the way they need to be set.

A word on sockets

PHP, Nginx, and memcached need to be able to pass data back and forth to each other. By default, this is done with TCP ports—PHP-FPM listens on one TCP port for Nginx to feed it scripts to execute; memcache listens for data from PHP-FPM on another TCP port. This means that communication between these different processes needs to be run through the network stack—ensuring maximum compatibility, but coming at the expense of speed. It's a lot faster to use Unix sockets for inter-process communication, which dispenses with the networking stack and lets processes talk within the kernel.

Wherever possible in this guide and in any subsequent guide, where there is inter-process communication to be done, we're going to channel it through Unix sockets instead of involving the networking stack. It's easy to set up and while it won't make much of a difference for a small personal site, it's a good thing to learn how to do.

Configuring the modules

If you take a peek inside /etc/php5/conf.d, you'll see several .ini files, each of which are used to tell PHP to load a specific module. For example, the contents of the mcrypt.ini file look like this:

 ; configuration for php MCrypt module extension=mcrypt.so 

The conf.d directory is parsed by PHP on start-up, and any file inside is included in the main PHP configuration. This is how modules are loaded. If you ever need to disable a specific module, you can comment out its extension line in that module's .ini file, or even delete the .ini file all together.

One change we might want to make is bumping up the amount of memory available to APC, our PHP opcode cache. Its default 32MB of RAM is fine for small sites with a single application, but if you have the free RAM, it's worth bumping up to 128MB. To do this, edit the file /etc/php5/conf.d/apc.ini and add the following line:

 apc.shm_size = 128 

We need to restart PHP to make the change effective, but we're going to hold off on that for now, since there are more changes to be made.

Configuring the PHP core

PHP-FPM's default configuration, and the default configuration of most of its modules, is mostly good out of the box. The changes we're going to make are almost all focused on switching things over to using Unix sockets instead of TCP ports for inter-process communication, as discussed above.

php.ini

First, open the file /etc/php5/fpm/php.ini for editing. We're going to make three specific changes in this file. Locate the line "post_max_size" and set it as follows:

 post_max_size = 2MB 

The default value of "8MB" means that PHP will accept POST requests of up to 8MB in size; we can reduce this to 2MB for now. This is one of the settings which affects the maximum size of files users are allowed to upload to the Web server (with applications that allow uploads or attachments, like WordPress or phpBB). We'll revisit this setting in a later article, but for now, cranking it down is fine.

Next, find session.save_handler and session.save_path and set them as follows:

 session.save_handler = memcache session.save_path = unix:/tmp/memcached.sock 

This reveals why we've installed memcached: we're going to use it to store our PHP sessions. A session is how PHP preserves information about a client over time; a client creates a session when they first connect to your Web application (your forum, or your blog, or whatever), and that session tracks whatever data needs to be tracked so that the Web server continues to recognize the client. Typically, a record of the session is stored on the client's computer using a cookie, and the Web server reads that cookie, learns who the client is, and applies the session data to that client. For a website like Ars Technica, which is based on a modified version of WordPress, PHP sessions ensure that users remain logged in during different visits to the site.

By default, sessions are stored in actual files in the /tmp directory. This can have some potential security implications, but the main drawback is that on a large site, using files for sessions can be slow since a site with many sessions means doing a lot of file system reads and writes. Memcached, though, uses RAM to hold its cache, and we can use memcached's RAM-based key-value store to hold our session data, freeing us from needing to deal with the file system and greatly speeding things up.

There are disadvantages to this approach, though: session data sometimes needs to hang around for a long time, and memcached's cache is RAM-based and isn't persistent across reboots (or even across memcached restarts), so session data stored in memcached is a lot more fragile. Personally, I prefer the speed of keeping as many things in RAM as possible over preserving the longevity of session data; you may have other priorities—if so, you'll want to leave the two session.save keys in their default settings.

www.conf

The big change to make in the www.conf file is switching it from listening on a TCP port to listening on a Unix socket. Open /etc/php5/fpm/pool.d/www.conf for editing and locate the listen setting in the file. It should be set by default to 127.0.0.1:9000, meaning that PHP is listening for incoming scripts on TCP port 9000. Change it as follows:

 listen = /var/run/php5-fpm.soc 

Additionally, a bit below that are two more lines we need to un-comment by removing the semicolon in front of them:

 listen.owner = www-data listen.group = www-data 

These two settings use the "www-data" user (the Nginx user) and its group as the owner of the Unix socket, which ensures that Nginx and only Nginx will be able to pass scripts to PHP-FPM for execution.

Splashing in the pool

One of the greatest strengths of PHP-FPM is its ability to scale its worker processes up and down as load on the server increases. PHP-FPM can have several "pools" of PHP handlers: one for each different Web application, with different numbers of worker processes and different rules about when to add more processes or kill idle processes.

Our needs are simple—we only need a single pool, defined in the www.conf file, and we can accept the default values for the number of PHP processes running and how and when processes are added or removed. However, if you want to change them, here are the settings in www.conf to modify:

  • pm = dynamic: Changes how PHP-FPM wil spin up PHP workers. The default setting uses the other settings to determine how to act. If you want a fixed number of PHP processes running, you can set this to static.
  • pm.max_children = 10: Sets a maximum of 10 PHP workers. If you find your site needs more, this can be adjusted upward.
  • pm.start_servers = 4: Sets how many PHP workers are activated when PHP-FPM starts up.
  • pm.min_spare_servers = 2: Sets the minimum number of idle workers that PHP-FPM will try to keep standing by.
  • pm.max_spare_servers = 6: Sets the maximum number of idle workers PHP-FPM will allow to remain alive before it starts terminating processes.

Again, the default values of these settings are fine for a small site.

Enlarge / The virtual machine I've been building with the tutorial, with PHP-FPM master and worker processes highlighted. (This process display is htop.)

Configuring memcached

The last thing we need to do before we can bring our PHP configuration to life is ready memcached to accept connections on a Unix socket. We've already told PHP-FPM where to send its session data, and now we just need to tell memcached to listen at that location.

Open /etc/memcached.conf for editing. Locate the connection port line and comment it out by placing a #in front of it:

 # Default connection port is 11211 # -p 11211 

Do the same with the listen address:

 # Specify which IP address to listen on. # -l 127.0.0.1 

Then, add the following lines at the bottom of the file:

 # Listen on a Unix socket  -s /tmp/memcached.sock  -a 666 

This sets up a Unix socket for memcached and sets permissions on the socket so that it can be accessed by other processes.

Plugging it into Nginx

Now for the last part: enabling Nginx to take advantage of the PHP setup we have just finished crafting. This is going to be a matter of telling Nginx about the Unix socket to which it needs to send PHP scripts; we also need to set some parameters so Nginx knows how to treat the data it's sending along.

Defining the socket as an upstream variable

We're going to first define our PHP as an "upstream" server variable, so that we can refer to it by name instead of by its longer path. The easiest way to do this is to create a new file in /etc/nginx/conf.d/ named php-sock.conf, and paste the following lines into it:

 upstream php5-fpm-sock {         server unix:/var/run/php5-fpm.soc; } 

Attacking errors before you get them

Next, open the file /etc/nginx/fastcgi_params and paste in the following parameters at the bottom:

 fastcgi_buffer_size 128k; fastcgi_buffers 4 256k; fastcgi_busy_buffers_size 256k; 

These settings, which adjust the buffer sizes allocated by Nginx for FastCGI, can help resolve errors with some Web applications particularly those that link to external services like Facebook or Twitter for log-in data. Adding this now might avoid trouble later, even though we don't yet have any Web apps installed.

Turning on PHP for virtual hosts

Lastly, we need to turn on PHP support for your website. This is done by editing the virtual host files; if you have more than one website (and thus more than one virtual host file), you can control which websites use PHP and which don't by selectively adding PHP to the virtual host files.

We only have one website on our test server, defined by the /etc/nginx/sites-available/www file, but that doesn't necessarily mean that we have to enable PHP everywhere. Nginx uses the concept of locations here, allowing you to constrain the areas on the server that are and are not allowed to contain active content. The simplest thing to do is to allow the universal execution of PHP scripts, by adding this location inside of the virtual host's server section:

 location ~ \.php$ {         try_files $uri =404;         allow 192.168.1.0/24;         allow 127.0.0.1;         deny all;         include fastcgi_params;         fastcgi_pass php5-fpm-sock;         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;         fastcgi_intercept_errors on;         } 

As discussed in our original article, Nginx locations don't have to be real "locations"—they don't have to be a file or a directory. Rather, Nginx locations use regular expressions to match patterns. Here, we're defining a location that means "every file everywhere that ends in .php", and then putting our parameters there. When Nginx is asked to serve a file that matches that location—that is, a file ending in ".php"—it will pass the file to the PHP-FPM socket. Here's what each of those lines does:

  • allow and deny: Prevents this location from being accessible outside your LAN (obviously, if you're not using 192.168.1.0/24 for your LAN, change that to the correct netblock). Once you've got something set up to actually serve out to the Internet, you can remove these lines; for now, we want them in place so that only you can execute PHP scripts.
  • try_files $uri =404: This line helps add a bit of extra security by guarding against a well-known configuration pitfall. Without this in place, an attacker could potentially force your server to execute malicious PHP code. The linked page contains an excellent explanation of how this works.
  • include fastcgi_params: Tells Nginx to include all the FastCGI parameters defined in /etc/nginx/fastcgi_params.
  • fastcgi_pass php5-fpm-sock: Tells Nginx where to actually pass its PHP content for execution, using the upstream variable we defined earlier.
  • fastcgi_param SCRIPT_FILENAME: Defines an additional FastCGI parameter, SCRIPT_FILENAME, which will be used by PHP-FPM so that it understands what part of the path it gets passed is the actual file to be executed, and what part is the file's location.
  • fastcgi_intercept_errors: If you've defined a custom set of error pages (which we so far haven't done in this tutorial set), this parameter lets Nginx display them instead of using its built-in error pages when there are problems. This setting doesn't do much without custom error pages, but it is useful to have set.

Variation with SSL/TLS

If you also have a secure site defined in the same virtual host file, you'll need to mirror the PHP location in the secure site's server block, with one very important additional line:

 fastcgi_param HTTPS on; 

This line needs to be appended to the PHP location for a secure server in order to ensure that Nginx and PHP-FPM correctly handle the scripts.

Testing it all out

There are quite a few services we need to restart at this point to make all of our changes live; it's probably easiest to just reboot the server:

 sudo reboot 

Once it comes back up, we can test out our PHP environment by asking it to report some information about itself. Navigate to your Web root location, which by default is /usr/share/nginx/html, and create a file there named phpinfo.php. Paste in the following:

 <?php phpinfo(); ?> 

Then, navigate to http://yourservername/phpinfo.php. You should see a page that looks like this:

Enlarge / This is what you'll see if PHP is working.

This page is automatically generated by PHP's phpinfo function and displays all kinds of interesting information about your server and your PHP setup. For now, the allow and deny lines in your PHP location directive will prevent non-LAN users from executing any PHP scripts, so this is safe to leave in place; once we actually get something built, we'll need to remove this file as it contains information potentially useful to attackers.

Wrapping up

We now have one of the two major components to having a dynamic website in place. There are actually many things you can do with just PHP—you can, for example, set up a flat-file wiki like DokuWiki. However, to make the Web server as useful as possible, we need a database.

Which database to use is a contentious choice, and we'll dive into that in the next installment of this series.

Veronika Zemanova Barbara Schoeneberger

No comments:

Post a Comment