Nginx Reverse Proxy for Apache2 (LAMP) with TLS/SSL on Debian

Install and configure Nginx to act as a reverse proxy for Apache over a TLS connection. Nginx, Apache and PHP configurations are covered. 

Normally an Nginx reverse proxy is on the Apache end which will cache all the static answers from the web server and reply to clients from its cache. Nginx is used for a benefit of Apache to reduce its load.

Software

Software used in this article:

  1. Debian Wheezy
  2. Nginx 1.2.1
  3. Apache 2.2.22
  4. OpenSSL 1.0.1e

Before We Begin

Nginx and Apache will be configured to support TLSv1, TLSv1.1 and TLSv1.2 only. SSLv2 and SSLv3 will be disabled. The following SSL ciphers will be enabled:

ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES256-GCM-SHA384
ECDHE-RSA-AES128-GCM-SHA256
ECDHE-RSA-AES256-SHA384
ECDHE-ECDSA-AES256-SHA384
ECDHE-RSA-AES256-SHA
ECDHE-ECDSA-AES256-SHA
ECDH-RSA-AES256-GCM-SHA384
ECDH-ECDSA-AES256-GCM-SHA384
ECDH-RSA-AES256-SHA384
ECDH-ECDSA-AES256-SHA384
ECDH-RSA-AES256-SHA
ECDH-ECDSA-AES256-SHA
DHE-RSA-AES256-GCM-SHA384
DHE-RSA-AES256-SHA256
DHE-RSA-AES256-SHA
ECDHE-ECDSA-AES128-SHA256
ECDHE-RSA-AES128-SHA256
ECDHE-RSA-AES128-SHA
ECDHE-ECDSA-AES128-SHA
DHE-RSA-CAMELLIA256-SHA
DHE-RSA-AES128-GCM-SHA256
DHE-RSA-AES128-SHA256
DHE-RSA-AES128-SHA
DHE-RSA-SEED-SHA
DHE-RSA-CAMELLIA128-SHA

Apache will be configured to listen on port 8080 for HTTP and on port 8081 for HTTPS. Nginx will use default HTTP and HTTPS ports.

Full LAMP stack is installed because of Mediawiki and WordPress. Note that Mediawiki and WordPress installations are beyond the scope of this article. Feel free to drop MySQL and PHP if you don’t need them.

It is assumed that:

  1. Default website resides under /var/www/.
  2. Mediawiki is installed under /var/www/wiki/.
  3. WordPress is installed under /var/www/wordpress/.

Also, in our case, /var/www is mounted on a separate encrypted partition with noexec, nodev and nosuid:

$ mount -l | grep www
/dev/mapper/data on /var/www type ext4 (rw,nosuid,nodev,noexec,relatime,user_xattr,barrier=1,data=ordered) [data]
  1. noexec: don’t set execution of any binaries on this partition (prevents execution of binaries but allows scripts).
  2. nodev: don’t allow character or special devices on this partition.
  3. nosuid: don’t set SUID/SGID access on this partition (prevents the setuid bit).

Installation

Apache2 + OpenSSL

# apt-get install --no-install-recommends apache2 openssl

MySQL + PHP5 (Optional)

# apt-get install --no-install-recommends mysql-server php5 libmysqld-dev \
libmysqlclient-dev php5-common php-pear php5-cli php5-curl php5-fpm php5-gd \
php5-mysql php-apc libapache2-mod-php5

Nginx

Install Nginx only when finished configuring Apache2, otherwise you’ll end up having two webservers fighting for port 80.

# apt-get install nginx

Configure Apache with HTTPS

Disable default website. We’ll use httpd.conf to define virtual hosts.

# a2dissite default

Generate and Install a Self-Signed SSL Certificate

We want to push everything through a secure TLS/SSL connection.

# mkdir /etc/ssl/webserver && cd /etc/ssl/webserver

Generate a self-signed certificate:

# openssl req -x509 -days 1825 -sha256 -nodes -newkey rsa:2048 \
-keyout ./server.key -out ./server.crt

Make the private key readable by the root user only:

# chmod 0600 ./server.key

Enable Apache Rewrite and SSL Modules

# a2enmod rewrite ssl

Configuration of /etc/apache2/conf.d/security

Server’s signature is enabled so we can tell which of the chained servers actually produced a returned error message in case there were any. ServerTokens is set to prod in order to prevent Apache’s version from being shown online.

ServerSignature on
ServerTokens prod
TraceEnable off
Header unset ETag
FileETag None
#Header always append X-Frame-Options SAMEORIGIN
#Header set X-Content-Type-Options nosniff
#Header set X-XSS-Protection "1; mode=block"
Header edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure
#Header set Content-Security-Policy "default-src 'self';"

The above configuration requires the header module to be enabled:

# a2enmod headers

Configuration of /etc/apache2/ports.conf

Listen on port 8080 for HTTP requests and on port 8081 for HTTPS requests.

NameVirtualHost *:8080
Listen 8080

<IfModule mod_ssl.c>
 Listen 8081
</IfModule>

<IfModule mod_gnutls.c>
 Listen 8081
</IfModule>

Configuration of /etc/apache2/httpd.conf

Make sure https.conf is included into the /etc/apache2/apache2.conf.

ServerName example.com

<VirtualHost *:8080>
 ServerAdmin [email protected]

 #rewrite HTTP to HTTPS
 RewriteEngine On
 RewriteCond %{HTTPS} off
 RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}

 ErrorLog ${APACHE_LOG_DIR}/error.log
 CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

<VirtualHost *:8081>
 ServerAdmin [email protected]

 SSLEngine on
 SSLCertificateFile    /etc/ssl/webserver/server.crt 
 SSLCertificateKeyFile /etc/ssl/webserver/server.key

 #ALL is a shortcut for "+SSLv2 +SSLv3 +TLSv1 +TLSv1.1 +TLSv1.2"
 #when using OpenSSL 1.0.1 and later
 SSLProtocol ALL -SSLv2 -SSLv3
 SSLVerifyClient none
 SSLVerifyDepth 1
 SSLHonorCipherOrder On

 #SSLLabs.com suggestion
 SSLCipherSuite EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:ECDH+AES256:DH+AES256:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:EDH+aRSA:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;

 DocumentRoot /var/www
 <Directory />
     Options FollowSymLinks -Indexes
     AllowOverride All
     Order Allow,Deny
     Allow from all
     SSLRequireSSL
 </Directory>

 ErrorLog ${APACHE_LOG_DIR}/error.log
 CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Check Apache Configuration Files for Syntax Errors

# apachectl configtest
Syntax OK

Show Virtual Hosts Configuration

# apachectl -t -D DUMP_VHOSTS
VirtualHost configuration:
wildcard NameVirtualHosts and _default_ servers:
*:8081                 example.com (/etc/apache2/httpd.conf:14)
*:8080                 is a NameVirtualHost
         default server example.com (/etc/apache2/httpd.conf:3)
         port 8080 namevhost example.com (/etc/apache2/httpd.conf:3)
Syntax OK

Restart Apache

# service apache2 restart

Configure PHP (Optional)

Create non world-writeable directories for temporary HTTP uploaded files as well as php session:

# mkdir -p -m 0750 /var/www/php_dir/temp
# mkdir -p -m 0750 /var/www/php_dir/session
# chown -R www-data:www-data /var/www/php_dir

If you are using PHPMyAdmin and get the following error:

Cannot start session without errors, please check errors given in your PHP and/or webserver log file and configure your PHP installation properly.

You may need something like this:

# chmod 1777 /var/www/php_dir/session

Open /etc/php5/apache2/php.ini and modify the following settings appropriately (use as a guidance only):

[PHP]
engine = On
disable_functions = exec,system,shell_exec,passthru
open_basedir = Off
expose_php = Off
max_execution_time = 600
max_input_time = 600
memory_limit = 256M
display_errors = Off
display_startup_errors = Off
track_errors = Off
html_errors = Off
error_log = "/var/log/php-errors.log"
register_globals = Off
post_max_size = 32M
magic_quotes_gpc = Off
file_uploads = On
upload_tmp_dir = "/var/www/php_dir/temp"
upload_max_filesize = 16M
max_file_uploads = 5
allow_url_fopen = Off
allow_url_include = Off
[Date]
date.timezone = "Europe/London"
[Session]
session.save_path = "/var/www/php_dir/session"

Configure Nginx as a Reverse Proxy

Configuration of the /etc/nginx/nginx.conf file is shown below.

As you may see, our dual-core Debian server is configured to serve around 1024 connections (2 worker process * 512 connections ) at one time.

user www-data;
worker_processes 2;
pid /var/run/nginx.pid;

events {
	worker_connections 512;
}

http {
	#BASIC SETTINGS
	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 30;
	types_hash_max_size 2048;
	server_tokens off;

        #LOGGING SETTINGS
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

	#GZIP SETTINGS
	gzip on;
        gzip_buffers 16 8k;
        gzip_comp_level 2;
        gzip_min_length 1024;
        gzip_http_version 1.1;
        gzip_proxied any;
        gzip_types application/x-javascript text/css text/plain text/javascript;

	#VIRTUALHOST SETTINGS
server {
	listen 80;
	server_name example.com www.example.com;
	access_log /var/log/nginx/access.log;
      	error_log  /var/log/nginx/error.log;       

	location ~ \.php$ {
		proxy_redirect off;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_pass http://localhost:8080;
        }

        #this is mainly for Mediawiki
	#as Mediawiki URLs look like: http://example.com/wiki/index.php/blablabla
	#we have to be sure that all these URLs get redirected to Apache
	#thanks to: http://forum.nginx.org/read.php?2,215093,215095#msg-215095
	location /wiki/index.php {
		proxy_redirect off;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_pass http://localhost:8080;
	}
        #Nginx should serve all files except the php ones
	location / {
		root /var/www;
	        index index.html index.php;  
	}
	location ~ /\.ht {
                deny all;
        }
        }
server {
	listen 443 ssl;
	server_name example.com www.example.com;
	access_log /var/log/nginx/ssl_access.log;
      	error_log  /var/log/nginx/ssl_error.log;       

	ssl on;
        ssl_certificate      /etc/ssl/webserver/server.crt;
        ssl_certificate_key  /etc/ssl/webserver/server.key;
        ssl_session_timeout  5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        #add_header Strict-Transport-Security max-age=15552000;
        #add_header Content-Security-Policy "default-src 'self';
        #add_header Public-Key-Pins 'pin-sha256="";max-age=300''

	#SSLLabs.com suggestion
        ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:ECDH+AES256:DH+AES256:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:EDH+aRSA:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;
        ssl_prefer_server_ciphers on;

	location ~ \.php$ {
		proxy_redirect off;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_pass https://localhost:8081;
        }
	location /wiki/index.php {
		proxy_redirect off;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_pass https://localhost:8081;
	}
        #Nginx should serve all files except the php ones
	location / {
		root /var/www;
	        index index.html index.php;
	}      
	location ~ /\.ht {
                deny all;
        }
        }
}

Restart Nginx

# service nginx restart

Configure Iptables

# iptables -A INPUT -p tcp -m multiport --dport 80,443 -j ACCEPT

Test SSL Server on SSLLabs

Free online test service: https://www.ssllabs.com/ssltest

Related Posts

Prevent Logjam in Apache 2.2 on CentOS 6

2 thoughts on “Nginx Reverse Proxy for Apache2 (LAMP) with TLS/SSL on Debian

  1. Hello Tomas!

    Thanks for the great article!
    I see you’ve enabled SSL on both Apache and Nginx sides. This looks reasonable, but doesn’t it leads to duplicate SSL negotiations and awful performance issues?
    I’m fighting similar case on my side now and as far as I’ve learned it, the better option seems to set up SSL to be handled by Nginx (it has much more reach HTTPS/SSL features) and set up Apache virtual host as non-SSL host.

    Have you got something about this?
    Thank you! :)

Leave a Reply

Your email address will not be published. Required fields are marked *