Apache and Let's Encrypt best practices for security.


Last updated: November 25, 2017.

This is a simple multi-part guide for configuring Apache and Let's Encrypt with optimal security settings under Ubuntu 16.04+.

Example resources - apache2.conf, aaronhorler.com.conf, and aaronhorler.com-le-ssl.conf.

Configuring Apache

This assumes you have installed Apache via apt.


1. Disable broadcasting of the Apache version

sudo nano /etc/apache2/apache2.conf

Append the following options to the end of the configuration file.

ServerTokens Prod

ServerSignature Off

This will prevent Apache from sending its version in the response header and on error pages.


2. Disable the ETag response header

Append the option below to the same configuration file as in point 1.

FileETag None


3. Disable uneeded HTTP methods.

Append the option below to the same configuration file as in point 1.

TraceEnable off

Also append this to every <Directory> directive.

<LimitExcept GET POST HEAD>
    deny from all
</LimitExcept>

This will disable all HTTP methods expect GET, POST, and HEAD.


4. Set the OCSP stapling cache

You will configure OCSP stapling later.

Append the option below to the same configuration file as in point 1.

SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000)


3. Enable the HTTP/2 module (optional)

sudo a2enmod http2

If the above command produces an error, you need to upgrade to a newer version of Apache.

Using apt, add Ondřej Surý's Apache2 PPA and upgrade.

sudo add-apt-repository ppa:ondrej/apache2

sudo apt update

Upgrading using this method should not affect your existing configurations, but you should backup as a precaution.

sudo apt --only-upgrade install apache2

This should now work.

sudo a2enmod http2

Except - HTTP/2 will not actually work. Apache, by default, uses the prefork MPM - which does not support HTTP/2.

Switch to the event MPM.

sudo a2dismod mpm_prefork

sudo a2enmod mpm_event

If you're using PHP, it will not function with the event MPM. This can be resolved by switching to PHP-FPM. Remove your current PHP package first.

sudo apt install php7.0-fpm

sudo a2enmod proxy_fcgi setenvif

sudo a2enconf php7.0-fpm



Running Let's Encrypt

This requires you've setup at least one Apache VirtualHost. See this guide.


1. Install Let's Encrypt

sudo apt update

sudo apt install letsencrypt


2. Run Let's Encrypt

sudo letsencrypt --redirect --must-staple --rsa-key-size 4096 -d example.com -d www.example.com

This command will request a certificate with the OCSP Must-Staple flag set, and produce an RSA key with a large keysize of 4096 bits.


3. Use an ECDSA private key instead (very optional)

ECDSA is a faster and more secure alternative to RSA.

I recommend you test Let's Encrypt normally first.

First, create a new directory and move to it.

sudo mkdir -p /etc/letsencrypt/ecdsa/example.com

cd /etc/letsencrypt/ecdsa/example.com

Now, generate an ECDSA private key.

openssl ecparam -genkey -name secp384r1 > ./privkey.pem

And a Certificate Signing Request (CSR).

openssl req -new -sha512 -key privkey.pem -subj "/C=AU/ST=VIC/O=RMIT/CN=example.com" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:example.com,DNS:www.example.com\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05")) -out example.com.csr

Replace every instance of example.com with your domain.

Replace AU with an acronym for your country of residence, or server location.

Replace VIC with an acronym for your state or other locality of residence, or server location.

Replace RMIT with your organisation, or simply your name.

In case you were wondering, 1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05 will be used to request the certificate have the Must-Staple flag set. Do not alter this.

Request a certificate from Let's Encrypt.

You may need to temporarily stop Apache so that letsencrypt can bind to port 443.

sudo service apache2 stop

letsencrypt certonly --standalone --csr example.com.csr

Finally, set correct permissions on this directory and its contents.

sudo chown -R root: /etc/letsencrypt/ecdsa/

sudo chmod -R 700 /etc/letsencrypt/ecdsa/

sudo chmod 600 /etc/letsencrypt/ecdsa/*/*

And start Apache.

sudo service apache2 start



Configuring Apache VirtualHosts

This requires you've run Let's Encrypt.


1. Replace the Let's Encrypt-generated configuration file.

Let's Encrypt generates a configuration file for your VirtualHost. We're going to heavily change it.

First, remove the generated file.

sudo rm -f /etc/sites-available/example.com-le-ssl.conf

Now you can manipulate my template VirtualHost configuration to your liking.

<IfModule mod_ssl.c>
<VirtualHost *:443>
    # Uncomment this to enable HTTP/2. You must have enabled the module.
    # Protocols h2 http/1.1

    # Enable TLS 1.2 only, with modern ciphers and generally good practices.
    SSLEngine On
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    SSLHonorCipherOrder On
    SSLCompression Off
    SSLSessionTickets Off
    SSLOptions +StrictRequire   

    # Enable OSCP stapling.
    # You must have configured SSLStaplingCache in the global configuration file.
    SSLUseStapling On
    SSLStaplingResponderTimeout 5
    SSLStaplingReturnResponderErrors Off

    # Secuirty headers.
    # Write your CSP. 
    # This will break your site. You'll need to use trial and error. 
    # Do not use 'unsafe-inline' or 'unsafe-eval', and no not change default-src to anything but 'none'.
    Header always set Content-Security-Policy "default-src 'none'; base-uri 'none'; connect-src 'none'; font-src 'none'; frame-ancestors 'none'; frame-src 'none';  img-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; sandbox; script-src 'none'; style-src 'none'; upgrade-insecure-requests; worker-src 'none'"

    # Enable HSTS
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"

    # Instruct browsers not to render your domain in frames, iframes, or objects.
    Header always set X-Frame-Options "DENY"

    # Instruct browsers not to send the Referer header.
    Header always set Referrer-Policy "no-referrer"

    # Instruct browsers not to MIME sniff.
    Header always set X-Content-Type-Options "nosniff"

    # Enable XSS filtering, and block rendering of the page if an attack is detected.
    Header always set X-XSS-Protection "1; mode=block"

    # Opt-out of DNS prefetching.
    Header always set X-DNS-Prefetch-Control "off"

    # Ensure that all cookies have the secure flag set.
    Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4"

    # Change this to your domain.
    ServerName example.com
    ServerAlias www.example.com

    ServerAdmin admin@example.com
    DocumentRoot /var/www/example.com/html

    # Standard logging.
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
    LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common

    # Standard Let's Encrypt configuration.
    # Remove this if you're using ECDSA.
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    # Uncomment this for my ECDSA configuration.
    # For SSLCertificateFile use the 0000_chain.pem with the highest number.
    # SSLCertificateFile /etc/letsencrypt/ecdsa/example.com/0001_chain.pem
    # SSLCertificateKeyFile /etc/letsencrypt/ecdsa/example.com/privkey.pem
</VirtualHost>
</IfModule>

This configuration complies with Mozilla's modern SSL/TLS recommendations. You can find the intermediate recommendations for SSLProtocol and SSLCipherSuite here.

The modern recommendations will prevent access from certain older clients by, for example, disabling every SSL/TLS protocol except TLS 1.2.


3. Configure and set security headers in my template.


HTTP Strict Transport Security (HSTS)

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"

Instruct browsers to remember for two years (63072000 seconds) to force HTTPS-only connections on your domain, and every subdomain. Optionally you can include the preload option and add your domain to this list to enforce your domain's HSTS policy permanently as part of future browser releases.

Warning: By enabling HSTS, you are comitting to HTTPS (effectively) permanently.


Content Security Policy (CSP)

Header always set Content-Security-Policy "default-src 'none'; ...

Instruct browsers what resources are allowed. Refer to this for all options.


Referrer-Policy

Header always set Referrer-Policy "no-referrer"

Instruct browsers not to send the Referer header.


X-Frame-Options

Header always set X-Frame-Options "DENY"

Instruct browsers not to render your domain in frames, iframes, or objects.


X-XSS-Protection

Header always set X-XSS-Protection "1; mode=block"

Enable XSS filtering, and block rendering of the page if an attack is detected.


X-Content-Type-Options

Header always set X-Content-Type-Options "nosniff"

Instruct browsers not to MIME sniff.


4. Create your new VirtualHost file, and reboot Apache.

Once you've modified my template, create a new file with the same name and paste your configuration.

sudo nano /etc/sites-available/example.com-le-ssl.conf

sudo service apache2 restart



Securing PHP slightly (optional)

This is only required if you have PHP installed, and assumes PHP 7.0+.


1. Create a user.ini configuration file.

sudo nano /etc/php/7.0/apache2/conf.d/user.ini

Note: The location of the conf.d directory may differ depending on the exact version of PHP.

# Cookie security.
session.cookie_httponly = 1
session.use_only_cookies = 1
session.cookie_secure = 1

# Disable the X-Powered-By response header.
expose_php = Off

# Disable the on-page display of PHP errors.
display_errors = Off

# Disable URL fopen and includes.
allow_url_fopen = Off
allow_url_include = Off

# Disable file uploads.
file_uploads = Off

After applying any of these changes, reboot Apache.

sudo service apache2 restart



Comments are provided by Disqus. To respect user privacy, Disqus is only loaded on user prompt.

I recommend uBlock Origin to protect against Disqus tracking and advertising.