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:
- Debian Wheezy
- Nginx 1.2.1
- Apache 2.2.22
- 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:
- Default website resides under
/var/www/
. - Mediawiki is installed under
/var/www/wiki/
. - 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]
- noexec: don’t set execution of any binaries on this partition (prevents execution of binaries but allows scripts).
- nodev: don’t allow character or special devices on this partition.
- 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
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! :)
Agreed, it’s better to terminate SSL on a load balancer level, in this case Nginx.
Set SSL termination for your upstream, take a look here:
https://www.nginx.com/resources/admin-guide/nginx-tcp-ssl-termination/