Install Let’s Encrypt SSL Certificate for Apache on Ubuntu 22.04 LTS

Plus an introduction to basic website hardening

In my previous post, we installed WordPress on the LAMP stack (Linux, Apache, MySQL, and PHP). Now we’re going to take a look at installing an SSL/TLS certificate as well as some basic hardening before we move onto security modules that will help protect our site.

(Note: I am working on a Bash script to automate this for you, so I will add that section here when it is complete. For now, enjoy the post; I will cover it in a video this weekend).

In order to receive an SSL certificate, you need to have an FQDN (fully qualified domain name) because Let’s Encrypt does not issue certificates to IP addresses. Domain names are inexpensive at most of the registrars. Mine is $12 per year, for example. Once you have a domain name, you’ll need to set up an A record that points your domain name to your public IP address. If you’re hosting this website on an EC2 instance, that’s all you need to do; however, if you’re self-hosting, you’ll need to set up a DDNS (dynamic domain name system) server to update your A record as your residential IP address changes. Having said that, ensure that your server is accessible over port 80 for HTTP and port 443 for HTTPS. You can either forward the ports to the local IP address where you’re hosting your WordPress site, or if you’re on AWS, allow traffic on those ports.

Please note that self-hosting is inadvisable for beginners. Since WordPress makes up a large percentage of available websites on the internet, it is frequently targeted for attacks; therefore, you risk exposing your home network. In which case, your server should be located on a separate subnet from your home LAN and firewalled appropriately. That is outside the scope of this blog post.

Since the Snap package manager comes pre-installed with Ubuntu, we can use it to install Certbot for generating a cert and then issue it to our website:

sudo snap install certbot --classic

A successful installation looks something like, “certbot 2.6.0 from Certbot Project (certbot-eff✓) installed”, and you can verify the Certbot package version with:

certbot --version

Next we’ll create a symbolic link from the snap binary to the user resources binary to allow for execution.

sudo ln -s /snap/bin/certbot /usr/bin/certbot

And because we’re on Apache, we’ll install the certificate with:

sudo certbot --apache

During installation, Certbot will ask you to enter an email address for urgent renewal and security services. Enter your email. Then you’ll be asked to read the ToS. Agree to the ToS, and then enter Y/N on whether you’d be willing to share your email with the EFF. Since I’m already a member, I select “No,” but I’ll leave that up to you. Next, enter the domain name you wish to use (this will be the domain name you purchased and pointed to a public IP address with an A record).

Congrats! You’ve successfully installed an SSL/TLS certificate on your website. Verify it’s working by navigating to https://yourdomain.com and you should see a lock in the address bar. Also check to make sure renewals are working:

sudo certbot renew --dry-run

You’ll see a few progress updates in the terminal and an, “all renewals succeeded” if everything is working correctly. Certificates are valid for 90 days and you can renew at 60. I advise setting up a cronjob that renews your certificate automatically.

But we’re nowhere near finished. This only protects connections directly to your domain url – meaning you can still type in https://your-domain-public-ip-address and connect to the server, and you can also connect via HTTP. This is still insecure. Yikes! This is why we enabled rewrites when we installed Apache server. When configured, rewrites allow us to take requests made to the IP address or HTTP resources on our site and redirect them to the HTTPS resources, making our site more secure. But there’s still more: we also enabled the headers module when we installed Apache, and this will allow us to enforce HTTP Strict Transport Security (HSTS), so anyone visiting the website will be forced to use HTTPS. Finally, we’ll allow only newer versions of SSL/TLS to establish secure connections to our website.

While it sounds like a lot, it’s relatively easy to implement. In addition to issuing your site a certificate, Certbot also created a new configuration in your /etc/apache2/sites-available directory.

sudo nano /etc/apache2/sites-available/wordpress-le-ssl.conf

This file is based on the wordpress.conf file we created when we installed WordPress. Your SSL configuration should look something like the below.

<IfModule mod_ssl.c>
<VirtualHost *:443>
    DocumentRoot /var/www/html/wordpress

    <Directory /var/www/html/wordpress>
        Options FollowSymLinks
        AllowOverride Limit Options FileInfo
        DirectoryIndex index.php
        Require all granted
    </Directory>
    <Directory /var/www/html/wordpress/wp-content>
        Options FollowSymLinks
        Require all granted
    </Directory>

ServerName yourdomain.com
SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

We’re going to make some changes here that will make it even more secure. I’ve added comments to explain the changes.

<IfModule mod_ssl.c>
<VirtualHost *:443>
        
        DocumentRoot /var/www/html/wordpress
        
        # I've enabled symbolic links for the entire file structure of the website
        # because we have to explicitly deny access to specific directories. 
        <Directory /var/www/html/>
            Options FollowSymLinks
            AllowOverride All
            Require all granted
        </Directory>
        
        # Direct error logging to the Apache server's error.log file
        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

        # Strict-Transport-Security header
        Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
        # XSS protection
        Header edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure
        Header set X-XSS-Protection "1; mode=block"
        # Clickjacking protection
        Header always append X-Frame-Options SAMEORIGIN


RewriteEngine On
#Enabling rewrites to direct IP-address and HTTP resources to domain HTTPS
RewriteCond %{HTTP_HOST} ^\d+\.\d+\.\d+\.\d+$
RewriteRule ^ https://yourdomain.com%{REQUEST_URI} [L,R=301]
# Require HTTP v1.1 over the less secure v1.0
RewriteCond %{THE_REQUEST} !HTTP/1.1$
RewriteRule .* - [F]

# Enforces TLS version 1.2 and above with a strong cipher suite to secure communication
SSLProtocol -All +TLSv1.2 
SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH

# Unchanged: default directives for Let's Encrypt when certificate was issued
ServerName yourdomain.com
SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

This takes care of most use cases, but we need to check out the HTTP configuration to make sure any requests made to the HTTP resources via IP address are also redirected.

sudo nano /etc/apache2/sites-available/wordpress.conf

The file was modified by Certbot when we installed the certificate, but you’ll also change it to the following (again, adding comments where changes have been or need to be made).

<VirtualHost *:80>
        DocumentRoot /var/www/html/wordpress
        # Add ServerName and permanent redirects
        ServerName yourdomain.com
        Redirect permanent / https://yourdomain.com

        <Directory /var/www/html/>
            Options FollowSymLinks
            AllowOverride All
            Require all granted
        </Directory>

        # Direct error logging to the Apache server's error.log file
        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

# Certbot added rewrite rules to forward http traffic
RewriteEngine On
RewriteCond %{SERVER_NAME} =yourdomain.com
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
# Require HTTP v1.1 over the less secure v1.0
RewriteCond %{THE_REQUEST} !HTTP/1.1$
RewriteRule .* - [F]
</VirtualHost>

This makes our website secure from a connection standpoint. Anytime you log into the main page or phpMyAdmin, you can feel safe knowing that your connection is secure. So what else can we do?

Let’s start by changing the WordPress Address (URL) and the Site Address (URL) in settings. When you log into WordPress, find the Settings menu on the left-hand side, and click on General. There you’ll likely see those two sections still configured to use the HTTP version of your public IP address – make sure to change them to https://yourdomain.com.

Next we’re going to restrict administrative access to both our admin directory as well as our main and phpMyAdmin login pages. Hopefully you’ve set strong passwords, but this will provide an added layer of protection by preventing brute-force attacks.

Open up the wordpress.conf file again.

sudo nano /etc/apache2/sites-available/wordpress.conf

Add the following after the main configuration (underneath the closed </IfModule> tag):

<location /wp-admin>
        Order Deny,Allow
        deny from all
        Allow from 127.0.0.1
        #Allow from a.local.ip.address if self-hosting
        Allow from your.public.ip.address
</location>
<location /wp-login.php>
        Order Deny,Allow
        deny from all
        Allow from 127.0.0.1
        #Allow from a.local.ip.address if self-hosting
        Allow from your.public.ip.address
</location>
<location /phpmyadmin>
       Order Deny,Allow
       deny from all
       Allow from 127.0.0.1
       #Allow from a.local.ip.address if self-hosting
       Allow from your.public.ip.address
</location>

Add the same configuration to your /etc/apache2/sites-available/wordpress-le-ssl.conf as well.

Next we want to suppress the Apache and OS version we’re using because Apache’s default behavior when encountering things like forbidden errors is to display them. I don’t remember the statistic, but a lot of vulnerable websites run outdated software. The server and OS version shines a light on where to look for vulnerabilities. Moral of the story: keep your site updated!

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

Find the following and make changes:

ServerTokens Prod
ServerSignature Off
TraceEnable Off

We can also open up the error pages file:

sudo nano /etc/apache2/conf-available/localized-error-pages.conf

And customize 404 (Not Found) and 403 (Forbidden) errors. You can customize the responses for other errors, but for now we’ll just modify these two. You can direct both errors to a file contained in your WordPress file structure or simply leave the user with a plain-text response. I choose to point to a page for my 404 errors but display text for 403s. It’s up to you.

ErrorDocument 404 /path/to/404.html
ErrorDocument 403 "FORBIDDEN: You do not have access to this resource."

Next we’re going to limit the types of requests your server will respond to. Open up your .htaccess file:

sudo nano /var/www/html/wordpress/.htaccess

Add the following directly at the top (above #BEGIN WordPress):

<LimitExcept GET POST HEAD OPTIONS>
    Deny from all
</LimitExcept>

These will ensure that your server will drop things like CONNECT requests (which I’ve seen in my server logs) where a remote IP tries to establish a connection through your server to somewhere else, acting as a proxy.

And that’s it! We’ve taken some basic precautions to make our site more secure. Try entering http://yourdomain.com, or http://your-domain-ip-address, and make sure it redirects every time to HTTPS. Try entering pages that don’t exist to verify the 404 error page is displaying properly. Try to log in from your cell phone (while NOT connected to your home’s network) to verify access is forbidden.

While there are many plugins that do a lot of these things for you, I find that with a little elbow grease, the same functionality can be implemented from the command line. Always remember to back up your site after making significant changes, and don’t forget to keep WordPress updated!

In my next blog post, I’ll cover three great tools that will add an additional layer of protection: three Apache modules called mod_evasive (DoS/DDoS protection), ModSecurity (Web Application Firewall), and Fail2ban (an IPS framework that let’s us ban malicious or suspicious IP address based on their behavior).