Hardening WordPress

Published: 2015-Oct-23
Updated: 2016-May-11

Applications configured

Ubuntu Server 14.04 – 16.04
Apache 2.4.20
MySQL 5.6.28
PHP 7.0.5
OpenSSL 1.0.2g
WordPress 4.5
Sendmail 8.14.9
OSSEC 2.8.3
apt-transport-tor 0.2.1
Let’s Encrypt

The TLS configuration in this article will get you a Qualys SSL Labs perfect A+ (100/100/100/100).


Things that should be in this guide but I still haven’t learned well:

— HPKP (key pinning, it still scares me)
— DNSSEC (my registrar doesn’t support it, serious wtf)
— Grsecurity kernel patches
— Torify Sendmail

Regularly test your transport security here: https://www.ssllabs.com/ssltest/analyze.html

Regularly test your security headers here: https://securityheaders.io/


sudo vim /etc/ssh/sshd_config

Comment out these lines:

#HostKey /etc/ssh/ssh_host_dsa_key
#HostKey /etc/ssh/ssh_host_ecdsa_key

Edit these lines:

ServerKeyBits 4096
LoginGraceTime 30
PermitRootLogin no

Add these lines:

Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr

MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256

KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256

Done. Then:

sudo service ssh restart


sudo ufw limit 22/tcp
sudo ufw allow 443/tcp
sudo ufw allow out 22/tcp
sudo ufw allow out 25/tcp
sudo ufw allow out 53/udp
sudo ufw allow out 443/tcp
sudo ufw allow out 9050/tcp
sudo ufw deny out to any
sudo ufw enable
sudo ufw status verbose

Or in one line…

sudo ufw limit 22/tcp && sudo ufw allow 443/tcp && sudo ufw allow out 22/tcp && sudo ufw allow out 25/tcp && sudo ufw allow out 53/udp && sudo ufw allow out 443/tcp && sudo ufw allow out 9050/tcp && sudo ufw deny out to any && sudo ufw enable && sudo ufw status verbose

This UFW (iptables) rule set makes it so brute forcing SSH won’t work and allows all HTTPS traffic. These rules also make it so no traffic can leave the web server unless it is SSH (22), SMTP (25), DNS (53), HTTPS (443), or Tor Socks (9050) traffic. Most web servers do not go as far as block all outbound traffic by default, but it is important in case the web server does become compromised.

I would usually allow outbound HTTP traffic because the Ubuntu update repositories require HTTP… they’re not HTTPS which is extremely sad. However, we will be Torifying Apt so that’s why we allow outbound 9050/tcp. If you don’t want to Torify Apt, you’ll need to allow outbound 80/tcp instead of 9050/tcp.

I allow outbound SMTP traffic because I use Sendmail to send TLS encrypted email notifications.

I do not allow any inbound HTTP traffic, and it is because of HSTS Preload. All major browsers know to connect to my website via HTTPS and not HTTP.

Patch (securely)

Make sure your OS is current then restart:

sudo apt-get update

sudo apt-get dist-upgrade

sudo shutdown -r now

Get updates over Tor instead of HTTP. First install and configure Tor.

sudo vim /etc/apt/sources.list

Add these lines to the bottom:

deb http://deb.torproject.org/torproject.org wily main


sudo gpg --keyserver keys.gnupg.net --recv 886DDD89

sudo gpg --export A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89 | sudo apt-key add -

sudo apt-get install tor deb.torproject.org-keyring apt-transport-tor -V

Backup sources.list:

sudo cp /etc/apt/sources.list /etc/apt/sources.bak

Edit sources.list with tor+http:

sudo vim /etc/apt/sources.list

For example (add “tor+” to all active lines):

deb tor+http://us.archive.ubuntu.com/ubuntu/ wily main restricted

Add these PPAs so that Apache, MySQL and PHP will be up to date:

sudo add-apt-repository ppa:ondrej/apache2

sudo add-apt-repository ppa:ondrej/mysql-5.6

sudo LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php

Be sure to Torify the new PPAs.

sudo vim /etc/apt/sources.list.d/ondrej-ubuntu-apache2-wily.list
deb tor+http://ppa.launchpad.net/ondrej/apache2/ubuntu wily main


sudo vim /etc/apt/sources.list.d/ondrej-ubuntu-mysql-5_6-wily.list
deb tor+http://ppa.launchpad.net/ondrej/mysql-5.6/ubuntu wily main


sudo vim /etc/apt/sources.list.d/ondrej-ubuntu-php-wily.list
deb tor+http://ppa.launchpad.net/ondrej/php/ubuntu wily main

Done. Then:

sudo apt-get update

All repositories should now read tor+http://… because they are routed through Tor. If any hang, it is likely because a repo was missed (and needs tor+) and is trying to connect using outbound HTTP port 80.

Now install the apps:

sudo apt-get update && sudo apt-get install apache2 libapache2-mod-evasive unzip nghttp2 nghttp2-server mysql-server-5.6 php7.0 sendmail -V

Sendmail config

sudo vim /etc/mail/sendmail.cf

Edit or add these lines:


This configuration assures that TLS will be used when sending mail to a destination mail server (not STARTTLS!). However, you’d still be using self-signed certs. Regardless, ECDH+AES256 is the only cipher being set because I know my email server receiving my alerts will use it.

sudo service sendmail restart

I can verify the TLS encryption in my received email headers:

Received: from yawnbox.com (mail.yawnbox.com. [])
(version=TLS1_2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256/256);

After you install Let’s Encrypt certs, you may want to add these lines:

O ServerCertFile=/etc/letsencrypt/live/example.com/fullchain.pem
O ServerKeyFile=/etc/letsencrypt/live/example.com/privkey.pem
O DHParameters=/etc/ssl/private/dhparams_4096.dh.param

This would configure Sendmail to use the same certs that you will use for HTTPS, which is fine since it should be from the same TLD. However, if you have a mail server with valid certs already, you should use those certs instead here (like if your mail server certs are for mail.example.com).


vim /etc/php/7.0/apache2/php.ini

Uncomment and edit this line:

sendmail_path = /usr/sbin/sendmail -t -i


sudo service apache2 restart

Apache config

sudo vim /etc/apache2/mods-available/evasive.conf

Uncomment the first 6 lines. Change if needed. Edit the DOSEmailNotify line if you’d like to be alerted when a specific IP address gets blocked. Then:

sudo vim /etc/apache2/apache2.conf

Add these lines:

FileETag None
Header unset ETag
AuthnCacheSOCache shmcb

Edit this line:

Timeout 30

Done. Then:

sudo a2enmod headers http2 ssl socache_shmcb

Done. Then:

sudo vim /etc/apache2/conf-available/security.conf

Uncomment these lines:

Directory /
   AllowOverride None
   Require all denied

Add these lines:

Header always set X-XSS-Protection: "1; mode=block"
Header always set X-Permitted-Cross-Domain-Policies: "master-only"
Header always set Cache-Control: "private, no-cache, no-store, must-revalidate"
Header always set Pragma: "no-cache"
Header always set Expires: "-1"
Header always set X-Content-Type-Options: "nosniff"
Header always set X-Frame-Options: "sameorigin"
Header always set Content-Security-Policy: "default-src 'self'"

Edit these lines:

ServerTokens Prod
ServerSignature Off
TraceEnable Off

Done. Then:

sudo service apache2 restart

Apache TLS config

Start by Generating 4096-bit DHparams. This will take a while (5+ minutes) depending on your server’s processing capabilities. These three lines may be something worth scripting to recreate every month via cron:

sudo openssl dhparam -out /etc/ssl/private/dhparams_4096.tmp 4096

sudo mv /etc/ssl/private/dhparams_4096.tmp  /etc/ssl/private/dhparams_4096.pem

sudo cp /etc/ssl/private/dhparams_4096.pem /etc/ssl/private/dhparams_4096.dh.param

Done. Then:

sudo vim /etc/apache2/mods-enabled/ssl.conf

Add these lines:

SSLCompression off
SSLSessionTickets off
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
SSLStaplingCache shmcb:/var/run/ocsp(128000)
SSLOpenSSLConfCmd DHParameters /etc/ssl/private/dhparams_4096.pem
SSLOpenSSLConfCmd Curves secp384r1

Change these lines:

SSLRandomSeed startup file:/dev/urandom 4096
SSLRandomSeed connect file:/dev/urandom 4096

Uncomment and/or edit these lines:

SSLHonorCipherOrder on
SSLProtocol -all +TLSv1.2

Let’s Encrypt will modify your Apache default-ssl.conf file for you. After installing Let’s Encrypt, I made sure to create 4096-bit keys:

sudo ./letsencrypt-auto --apache -d example.com -d www.example.com --rsa-key-size 4096

Keep an eye on ECDSA key support.

Done. Then:

sudo vim /etc/apache2/sites-available/default-ssl.conf

Add these lines:

SSLEngine on
Header edit Set-Cookie ^(.*)$ $1;Secure;SameSite=Strict
Header always set Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"

Add these lines under “/Directory” but before “/VirtualHost”:

#Protocols h2 http/1.1
H2Direct on
H2MaxSessionStreams 20
H2MaxWorkerIdleSeconds 20
H2MaxWorkers 20
H2MinWorkers 10
H2ModernTLSOnly on
H2SerializeHeaders off
H2SessionExtraFiles 10
H2StreamMaxMemSize 128000
H2TLSCoolDownSecs 0
H2TLSWarmUpSize 0
H2Upgrade on
H2WindowSize 128000

Now that HSTS and HSTS Preload are configured, submit your domain to Google for getting added to the Chrome HSTS Preload list:


Doing so will eventually replicate to all other mainstream browsers. My experieince was that after submitting my domain for Preload, I had to wait a couple of weeks for it to show up in Chrome, and a couple more weeks to show up in Firefox. Tor Browser took the longest. Just be patient.

I keep the “Protocols h2 http/1.1” directive commented out because I can’t achieve a perfect Qualys SSL Labs score and force HTTP2. When I do, Chrome sometimes doesn’t load the website because the HTTP2 specification doesn’t accept some of the cipher suites with this configuration.

Also check out HTTP2 Explained.

Done. Then:

sudo a2ensite default-ssl.conf
sudo a2dissite 000-default.conf
sudo service apache2 restart

Basic MySQL hardening

sudo apt-get install mysqltuner
sudo mysqloptimize -p -u root -A -o
sudo mysqltuner

Make any of the recommended changes (then re-run mysqltuner):

sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf


sudo vim /var/www/example.com/wp-config.php

Add these lines:

define('WP_MEMORY_LIMIT', '2G');
define('WP_MAX_MEMORY_LIMIT', '2G');

The WP_MAX_MEMORY_LIMIT specifically addresses the administrative backend, which has a different memory limit from both WordPress and PHP configs.

Then, disable the HTML5 canvas data issue (noticeable in Tor Browser):


(thanks Wilton)

wp-admin > Appearance > Editor > functions.php

Add this to the bottom:

remove_action( 'wp_head', 'print_emoji_detection_script', 7 );

remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );

remove_action( 'wp_print_styles', 'print_emoji_styles' );

remove_action( 'admin_print_styles', 'print_emoji_styles' );

OSSEC and Sucuri

OSSEC is a powerful Host-based Intrusion Detection System (HIDS). Sucuri is a powerful WordPress plugin. This section gets both installed and gets them working together.

wget -U ossec https://bintray.com/artifact/download/ossec/ossec-hids/ossec-hids-2.8.3.tar.gz

tar -zxvf ossec-hids-*.tar.gz

cd ossec-hids-2.8.3/

sudo ./install.sh

During installation:

1- local
2- accept default
3- accept all defaults


sudo /var/ossec/bin/ossec-control start

sudo vim /var/ossec/etc/ossec.conf

Verify/change/add these lines:

email_notification yes /email_notification
email_to email@email.com /email_to
smtp_server localhost /smtp_server
email_from ossecm@email.com /email_from

This configuration should force the use of TLS encrypted emails if Sendmail is installed and configured.

sudo service ossec restart

Then, in the WordPress Dashboard, install and activate the “Sucuri Security” plugin.

Open the Sucuri Security dashboard and generate an API key. It will use your WordPress admin account/email. I had to manually email the provided email to get a key.


sudo touch /var/log/wordpress.log

sudo chown www-data:www-data /var/log/wordpress.log

In WordPress Dashboard, navigate to Sucuri Security > Settings > Log Exporter

Enter “/var/log/wordpress.log” and save.


sudo vim /var/ossec/etc/ossec.conf

Under “Files to monitor (localfiles)“, add these lines:

    log_format syslog /log_format
    location /var/log/wordpress.log /location
sudo service ossec restart

Be prepared for lots of email notifications, thankfully TLS encrypted. Be careful about what information is copied and stored by your email provider.

WordPress Plugins

— Disable Comments
— Disable Google Fonts
— Disable XML-RPC