snork.ca: Welcome to the brown age of computing! Mainly Clear, 24.2°C [H: 29°C] - No precip

Making The Switch From Apache To Nginx 2016-04-23


Over the years, snork.ca has typically run on Apache web server. I have tried some other web servers along the way, but never found anything that had the same capacity for configuration, such as rewriting URLs and detailed logging controls. Recently though I have been considering the upgrade to Debian Jessie... since it has been available for a year now. The bad news is that the Jessie repositories have Apache v2.4 instead of v2.2 - and the configuration changes make it very irritating to make the migration. After running in to a bunch of problems with name-based virtual hosts and logging, I decided to give nginx another shot.

image The Nginx logo.

I found this article describing how to get it running with PHP on Jessie, and it worked better than I had expected. Having got it up and running I decided to see if I could configure it in a way that would make me happy enough to dump Apache. Here are the notes I took aong the way.

NOTE: There are a lot of people who insist that it is wrong, blasphemous, and sinful to log in to computers as an administrative account such as root. Since I am the only person to ever log in to the snork.ca server, and since I never run terrible applications such as a browser, and since almost every command I perform requires adminstrative priviledge, I am writing these instructions from the perspective of root. You'll also notice that most online instructions like these are written from the perspective of root as they do not include sudo on every command line. There is no added security benefit to using sudo, it is a psychological pacifier because anything you can destroy as root can also be destroyed by a user by simply adding sudo at the front of it. If you do not like that I log in to my server as root, feel free to not read these instructions.

Install Nginx

Following the instructions linked above, I updated apt and any packages that needed updating. Then added the nginx singing key and added their repository to my sources.list, and finally installed it from their repo.

# apt-get update && apt-get upgrade
# wget https://nginx.org/keys/nginx_signing.key
# apt-key add nginx_signing.key
# echo 'deb http://nginx.org/packages/debian/ jessie nginx' >> /etc/apt/sources.list
# echo 'deb-src http://nginx.org/packages/debian/ jessie nginx' >> /etc/apt/sources.list
# apt-get update && apt-get install nginx

I then edited the nginx.conf file to make a few basic changes. Setting the user to www-data seemed like it shouldn't be necessary but since my web site files already existed in /var/www and since they were already owned by www-data I figured it made sense. Setting the worker_processes to match the number of cores on my PC also made some sense and I assume it has some performance relevance. I also followed the instructions to disable the access log, because I planned to make separate access logs for each virtual host anyways. I didn't see why setting client_max_body_size was necessary but I did it anyways since it seemed harmless enough.

/etc/nginx/nginx.conf user www-data;
worker_processes 1;

access_log off;

client_max_body_size 12m;

As the instructions said, I deleted the sample config files in /etc/nginx/conf.d and created a new one which I named noname.conf (because I intend to use this as a default virtual host that will be used when anyone accesses the server with an incorrect DNS name or by IP address). I used the sample template provided, changed the root path to point at the files I already had, and I removed the server_name directive.

/etc/nginx/conf.d/noname.conf server {
   listen 80;

   root /path/to/your/website;
   index index.php index.html index.htm;

   server_name mydomainname.com www.mydomainname.com;

   location / {
      try_files $uri $uri/ /index.php;
   }

   location ~ \.php$ {
      try_files $uri =404;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      include fastcgi_params;
      fastcgi_pass unix:/var/run/php5-fpm.sock;
   }
}

Having not seen an nginx config file in about three years, I found the design of the "location" blocks to be interesting and I am assuming these will come in handy later when setting up logging and rewrites. The instructions for setting up php seemed easy enough, I included php5-gd and php5-mcrypt as described and also made the indicated changes to my php.ini (with the exception that I allowed 20MB uploads instead of 10MB).

# apt-get install php5-fpm php5-mysqlnd php5-gd php5-mcrypt

/etc/php5/fpm/php.ini upload_max_filesize = 20M
allow_url_fopen = Off
post_max_size = 12M

I didn't bother with the MariaDB stuff since I already had MySQL installed, though I do like the idea of dumping Oracle as long as MariaDB is not "heavier" and as long as the command syntax is essentially the same. Finally I restarted the services as shown and connected with my browser.

systemctl restart nginx.service
systemctl restart php5-fpm.service

Behold! It worked first try. I added another config file in /etc/nginx/conf.d and this time I included a server_name directive for snork.ca and pointed it to the appropriate files. This also worked first try and seemed quite fast... possibly even perceivably faster than Apache was. At this point I was happy enough to consider actually using it in place of the currently very frustrating Apache. It is pretty nice when copy/pasting instructions works this well on the first try!

Setting Up An SSL Virtual Host

So, for my next step I tried setting up an SSL based virtual host. I copied my existing "noname" config file to www.snork.ca.conf, I changed the port, I changed the path and server_name, and I added some SSL related directives. I quickly got frustrated with irritating error messages about not liking the certificate files I had copied over from my old server. Not being interested in screwing with different openssl formats, I decided to just setup a self signed cert from scratch.

# mkdir /etc/nginx/ssl && cd /etc/nginx/ssl
# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout nginx.key -out nginx.crt

Then the following directives were needed in my /etc/nginx/conf.d/www.snork.ca.conf file to make it use my new SSL cert. Unfortunately since this is not the free cert I got from StartSSL, I now get prompted to create a security exception in Firefox... but I don't care that much [yet] since the idea is to get rid of the old content anyways.

listen 443;
ssl on;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;

Controlling Logs

One of the things I like to do is use a custom user-agent string on my browser so that when I access my own web site it is simply not logged. This helps keep log files clean, especially when I am doing testing and might be refreshing a page over and over again. I also have some sections of the web site that are simply not logged at all, like the /chat directory for example. Finally I like to keep logs for search engine crawlers to a separate file. I found that setting an "if statement" like this inside a "location" section of my config files it would suppress the logging of my own traffic... though I was hoping that it would work in the main context.

if ( $http_user_agent ~* (MySecretUserAgent) ) {
   access_log off;
}

This means I would have to use this if statement in almost every "location section" of my config files. I figured that a map may be a more appropriate way to do this. So I found this article to show me how to make a map like this:

map $http_user_agent $logstuff {
default 1;
~MySecretUserAgent 0;
~Wanker 0;
}

...and then set my log file like this:

access_log /var/log/nginx/snork.ca.log main if=$logstuff;

It is important to note that the map must be in the main context (not in the server section) while the access_log setting is in the server section. I guess this means I will have to be careful if I want to use multiple maps on multiple virtual hosts and apply the rules differently in each. To avoid logging traffic from the localhost or LAN I can make a map like this:

map $remote_addr $logstuff {
default 1;
"127.0.0.1" 0;
"192.168.0.0/24" 0;
}

While sniffing around I found this article which seemed pretty interesting. Perhaps if I stick with nginx I might look at migrating my stopforumspam/iptables/ipset scripts to an nginx config file instead. For now, I am just happy to see that most of my logging controls can be done in nginx.

Rewriting URLs

I had a fair number of Apache RewriteRule directives for two main reasons. The first is because I wanted the end destination for hackers/scripts/spammers to be themselves and the second is to redirect legitimate requests from the old https://www.snork.ca/ site to the new location (which in most cases is now at the http://archive.snork.ca/ site). An example of the first reason would be this:

RewriteRule ^/wp- http://%{REMOTE_ADDR}/NOTICE-%{REMOTE_ADDR}-IS-INFECTED-OR-HACKING.html [NC,R,L,E=donotlog]

I would put this rule on a site that does not use wordpress, and does not have any files/directories at the root of it starting with "wp-". This rule redirects the request back to their own IP address (often a compromised wordpress web site) in hopes that the site admin would see the all caps entry in their log files and fix the problem. I doubt I ever actually alerted anybody to their site being hacked, but it made me feel good knowing that I was at least trying. An example of the second reason would be:

RewriteRule ^/2014/03/03/hmailserver-backups/ http://archive.snork.ca/?p=121 [R=301,L]

Which redirects the request to the correct new location of the information they were looking for. There is plenty of nginx official documentation on rewrites, but it is generally geared towards fixing wordpress fancy URLs, forcing https, or preventing access to some parts of a site. I just did some fiddling/searching and it seems that the best solution is the simple solution.

rewrite ^/oldpath/oldfile.html http://newsite.com/newarticle.html permanent;

There are some cases where an if statement is required though. Now I realize that there are plenty of people who like to link to the If Is Evil article but frankly this is one of the spots where nginx seems to fall short. The ability to control URL rewrites should be easier than Apache's already irritating design, not harder. In any case, here's an example of an if statement inside a server context (not a location context).

if ( $request_method = "POST" ){
   rewrite .* http://$remote_addr/NOTICE-$remote_addr-IS-INFECTED-OR-HACKING.html last;
}

Near as I can tell, if the last parameter of a "rewrite" statement is either last or redirect then the server will return a 302, while if the last parameter is permanent then it will return a 301.

Hopefully that will help someone else who is trying to make the switch from Apache to nginx. If you have customized your Apache install at all then there is really no quick and simple way to make the migration, you'll be picking away at little config options for a while whether you like it or not. Good luck eh.

Update: The Next Morning

I was actually setting all this stuff up on a replacement server rather that on the live one, of course because I didn't want to bomb the live one while working on it. To do this I was using ports 81 and 444 instead of the default ports of 80 and 443. This morning, I decided to give MariaDB a shot, and it went pretty well. It looks like the binary names and command line options are all the same, so it really appears to be a drop-in replacement. As a result, I have actually made this Nginx web server the live one. There are a few things left to clean up, like a custom 404 and some minor redirection, but it is running. Now all I gotta do is migrate from my old Wheezy/Postfix v2.9.6-2 to Jessie/Postfix v2.11.3-1 and Wheezy/Spamassassin v3.3.2-5+deb7u3 to Jessie/Spamassassin v3.4.0-6.

Made with Notepad++ & FastStone, without javascript, cookies, or Clippy's help. Hosted on Devuan with nginx & powered by NK shrooms.