Skip to main content

Hosting on the Web - How to Home Lab Part 6

Pulblished:

Updated:

Comments: counting...

In this segment you will learn how to configure your home lab to be accessible from the outside internet.

Get a Domain Name

You’re most likely going to want a domain name, unless you’re cool with saying things like “Hey, check out my website at 198.18.42.129!” You can find some free domain names out there if you look, most of them are branded (e.g. yourdomain.somecompany.com), and the others don’t usually offer very good tooling, so if you choose one of those be prepared for some extra work setting up DNS, possibly even buying a static IP address from your ISP.

What I Look for in a Domain Registrar

  1. Free Dynamic DNS
    • Dynamic DNS gives you API access to update your IP address if you don’t have a static IP, which can be automated with scripting.
  2. Free Whois Privacy
    • You don’t want your name and address available to anyone who knows how to use the whois tool, do you?
  3. TOTP Support (Time-based One Time Password)
    • Don’t use your cell phone for 2FA, it’s just not secure! There are tons of TOTP tools available these days, for example the free and open source KeePassXC password manager supports TOTP.

There are a lot of great domain registrars out there, and a lot of crummy ones too! Namecheap checks all the boxes for me, and I’ll be providing a script down below for dynamic DNS, I can only confirm that it works with Namecheap (I’m sure you could find some scripts or tooling for most of the other popular registrars if you searched for them).

If you should decide to use Namecheap, drop a domain into this form and hit Search to see if it’s available, the old firstnamelastname.com is always a good choice!

Note: This form is an affiliate link.

Find a domain starting at $0.88

powered by Namecheap

Don’t worry about setting anything else up yet, just make sure you own a domain.

pfSense Rules

We want to lock down the LAN network (which is actually production, technically). Keep in mind that pfSense rules are read first match top-down, and apply to traffic coming in to the interface, so the rules we are adding are to restrict traffic going in to the LAN interface and coming out of the LAN network (traffic to the WAN interface, or traffic going in to the LAN network from outside, is restrictive by default). Log in to your pfSense dashboard (https://IPADDRESS:8080).

Aliases

Head over to Firewall > Aliases > IP

IP Aliases

Non-Public

We’ll add an alias for Non-Public addresses, which includes the addresses reserved for private networks (RFC1918), the addresses reserved for special use (RFC5735), there are some other reserved address ranges but I’m not really concerned about them here, if you have some crazy IP addressing scheme in your home network just make sure it’s in the list.

These are addresses we will restrict the internal VMs from accessing to protect your home network. You may want to add 255.255.255.255/32 broadcast and 224.0.0.0/4 multicast to this list as well, but I don’t feel it’s necessary.

  • Name: Non_Public
  • Description: Non-public IP Addresses
  • Type: Networks
  • Network
    • 10.0.0.0/8
    • 172.16.0.0/12
    • 192.168.0.0/16
    • 169.254.0.0/16
    • 127.0.0.0/8

Save and apply the changes before moving on.

Rules

Head over to Firewall > Rules > LAN.

LAN Firewall Rules

Note: I’m not getting into IPv6 routing here, in fact we’re going to disable it in pfSense, there is a whole other layer of security considerations for IPv6.

Allow All on This Subnet

First we want to allow any traffic that is both from and to the LAN network, it doesn’t do anything right now, but it will save us a lot of drama down the road if we expand the network across multiple switches or something of that nature.

  • Action: Pass
  • Disabled: Unchecked
  • Interface: LAN
  • Address Family: IPv4
  • Protocol: Any
  • Source: LAN net
  • Destination: LAN net
  • Log: Unchecked
  • Description: Allow all this subnet

Allow Outbound DNS

Your VMs should be using pfSense for their DNS server by default, but some applications have their own default (like the Dynamic DNS script down below, which would fail without this rule enabled).

  • Action: Pass
  • Disabled: Unchecked
  • Interface: LAN
  • Address Family: IPv4
  • Protocol: TCP/UDP
  • Source: LAN net
  • Destination: any
    • Destination Port Range
    • From: DNS (53)
    • To: DNS (53)
  • Log: Unchecked
  • Description: Allow outbound DNS

Allow Outbound NTP

NTP is Network Time Protocol, we’re adding this for pretty much the same reasons as the DNS rule.

  • Action: Pass
  • Disabled: Unchecked
  • Interface: LAN
  • Address Family: IPv4
  • Protocol: TCP/UDP
  • Source: LAN net
  • Destination: any
    • Destination Port Range
    • From: NTP (123)
    • To: NTP (123)
  • Log: Unchecked
  • Description: Allow outbound NTP

Reject Non-Public

Note that since pfSense matches rules top-down, any rule below this one will only apply to public IP addresses, since non-public addresses are already being blocked.

  • Action: Reject
  • Disabled: Unchecked
  • Interface: LAN
  • Address Family: IPv4
  • Protocol: Any
  • Source: LAN net
  • Destination: Single host or alias, Non_Public
  • Log: Checked
  • Description: Reject Non-Public

Allow ICMP

Internet Control Message Protocol (ICMP) does more than just handle ping’s, in many cases it’s downright imperative for communication.

  • Action: Pass
  • Disabled: Unchecked
  • Interface: LAN
  • Address Family: IPv4
  • Protocol: ICMP
  • ICMP Subtypes: Any
  • Source: LAN net
  • Destination: any
  • Log: Unchecked
  • Description: Allow ICMP

Allow Web Ports

  • Action: Pass
  • Disabled: Unchecked
  • Interface: LAN
  • Address Family: IPv4
  • Protocol: TCP
  • Source: LAN net
  • Destination: any
  • Destination Port Range
    • From: Web_Ports
    • To: Web_Ports
  • Log: Unchecked
  • Description: Allow Web Ports

Delete Default Rules

You’ll want to remove the Default allow LAN to any rule, and Default allow LAN IPv6 to any rules now. Then go ahead and apply the changes.

Double check the order of the rules, getting this wrong could either break things or open security holes, it should look like this:

Firewall Rules Order

We’re going to get rid of that Anti-Lockout Rule at the top shortly, so don’t worry about it yet.

You may also want to add rules at the bottom of the list to allow outbound Git (Port 9418), and SSH (Port 22) access, although I would avoid allowing the latter unless it’s really necessary.

Settings

Disable IPv6

Head to System > Advanced > Networking, and uncheck the box for Allow IPv6, then click Save at the bottom of the page.

Disable IPv6

Disable Anti-Lockout Rule

Next head to System > Advanced > Admin Access, and check the box next to Anti-lockout, which makes sure LAN addresses can always access the pfSense admin interface, we don’t want that since we are accessing the admin interface from the WAN side. Again save at the bottom of the page.

Add a Port Forward from the Outside Internet

This part is difficult to explain, because there are so many edge devices and modems and routers out there, I’ll do my best here but if you get stuck let me know and I’ll try to help you out. If you don’t already have access to your edge device (usually a modem/router combo provided by your ISP), the Discover Your Network section of my Introduction to Networking article should get you there.

Also, if your Proxmox server is behind a second router, you’ll need to add a port forward on both routers (first to second, second to Proxmox), or just directly connect Proxmox to the first.

From that web interface, you’ll want to look for a setting called “Port Forward”, “Virtual Host”, or “Virtual Server”, which is usually in a menu called “Advanced”, “Routing”, “Firewall”, “Inbound Rules”, or some combination of those. Sometimes you just have to click through all the menus until you find the weird spot they decided to put it.

Once there, you’ll want to add two port forwards:

  • HTTP
    • Host: IP Address of pfSense VM
    • Port: 80 (or from 80 to 80)
    • Protocol: TCP
  • HTTPS
    • Host: IP Address of pfSense VM
    • Port: 443 (or from 443 to 443)
    • Protocol: TCP

Note: This is your mainline to the outside world, if you ever want to bring down your home lab make sure your port forwards get disabled as well.

Note: Some may be tempted to set their server as a DMZ host which essentially opens all ports to the internet, or to put their edge device in bridge mode which opens all ports for any device on the home network, please don’t do either of those things for security reasons.

Next, head over to YouGetSignal.com's Port Forwarding Tester and make sure both 80 and 443 are open to the outside world, it should auto-fill your public IP address (technically the IP address of your modem).

Set up External DNS

I’m going to assume you are using Namecheap here, if you aren’t the steps should be really similar except for the Dynamic DNS Script that may need a bit of work to connect with another service.

Configure Dynamic DNS Service

Log in to your account, and click the “Manage” button next to your domain from the Dashboard.

Manage Domain

Click the “Advanced DNS” tab, and then click “Add a new Record”.

DNS Settings

Choose the following options:

  • Type: A + Dynamic DNS Record
  • Host: @
  • Value: 0.0.0.0
  • TTL: Automatic
Dynamic DNS Record

Using @ for the host just means the apex domain, for example google.com is an apex domain, www.google.com is not. The 0.0.0.0 value will not route to anywhere, and this is intentional, when we run the Dynamic DNS script it should fill in the correct value and we’ll know it’s working.

Add one more DNS record:

  • Type: CNAME Record
  • Host: www
  • Value: mydomain.tld.
  • TTL: Automatic
CNAME Record

Note that the value should be your apex domain name with an extra dot at the end. This will direct requests for www.yourdomain.com to the apex domain (@), which will have the dynamic DNS entry. You could also change the value from www to *, which would direct www as well as any other subdomain like somethingelse.mydomain.com to the apex domain name.

Now go ahead and click the trash can icon on the right side of all the DNS records you didn’t just create to get rid of them. Scroll down the page to the Dynamic DNS section and copy your password, stash that in a text file for now because you’ll need it to set up the Dynamic DNS Script.

Dynamic DNS Password

Keep this password safe, anyone that has it will be able to change the IP address of your domain, that could be a disaster! Also, tuck away in your brain that this is where you would change that password if you needed to, by clicking the little refresh icon to the right of the password. The “Client Software” they offer works just fine but it’s only for Windows, so unless you have an always-on Windows machine (or even if you do) you’ll probably just want to use a script instead.

Dynamic DNS Script

Let me start by saying that if you’ve never used bash scripts before, this may look pretty daunting, but it’s easier than it looks so don’t panic! First, SSH into your NGINX server, I believe that is a logical place for handling dynamic DNS (but feel free to make a new VM for this if you wish, that would be the more secure option).

We need three files, a shell script to do the heavy lifting, a systemd service file to kick it off, and a systemd timer file to schedule it. You could use cron for this just as well, but I’ve really embraced systemd for how dynamic it can be.

You should also have the target server set up to send Email, and have the mailutils package installed, which I have a guide for here.

Bash Script

First things first, since this file will contain your Dynamic DNS password from your registrar, let’s create the file, and set secure permissions so that only root can read/write/execute it (run these commands as root).

touch /usr/local/updateServerIP.sh
chown root:root /usr/local/updateServerIP.sh
chmod 0700 /usr/local/updateServerIP.sh

Let me explain what this script does before we get into it, for each domain configured, each time the script is run it will:

  1. Check the current IP address via a DNS lookup (nslookup)
  2. Query myip.opendns.com for your current public IP address (dig)
  3. Compare the two, and stop running if they are the same (no change needed)
  4. Send an HTTPS request to update IP address (curl)
  5. Send the results in an Email (mail)
  6. Log all change actions to /var/log/updateServerIP.log

At the very beginning of the script, you’ll need to change you@yourdomain.com to your Email address, and towards the end you will find a block that looks like this:

# --------------------------------
# ------ BEGIN HOST DEFINITION ---
# --------------------------------
isdiff='1'
# --------------------------------
hostlist=("@")
domain="mydomain.com"
password="DynamicDnsSecretFromNamecheap"
# --------------------------------
ipcheck $domain $hostlist
if [ $isdiff -eq 0 ]; then
  update $hostlist $domain $password
fi
# --------------------------------
# -------- END HOST DEFINITION ---
# --------------------------------

You’ll want to change mydomain.com to your domain name (don’t use subdomains here, just the apex), and change DynamicDnsSecretFromNamecheap to, as you likely guessed, the Dynamic DNS secret you got from Namecheap.

The line hostlist=("@") you will probably want to just leave as is, you can specify one or more subdomains here if you have a complex layout with different subdomains going to different IP addresses, for example if you only wanted sub3 and sub7 to point to your public IP address you would use hostlist=("sub3" "sub7"), but most often you’d probably want to only use the apex (@) here and use CNAME records for the subdomains like we did with www.

One more thing you should know, is that you can copy the entire code block between BEGIN HOST DEFINITION and END HOST DEFINITION, and paste it underneath the existing one (just above the exit 0 line) to add another domain, you can add as many as you need!

Here is the full script:

/usr/local/updateServerIP.sh
#!/bin/bash

# Your Email address goes here
emailAddress="you@yourdomain.com"

logFile="/var/log/updateServerIP.log"
touch $logFile

logMessage() {
  case $1 in
  1)
    level="INFO"
    ;;
  2)
    level="WARN"
    ;;
  3)
    level="FAIL"
    ;;
  esac
  echo "[$(date)]     [$level] - $3" >>$logFile
  if [[ $2 == "1" ]]; then
    echo "[$(date)] [$level] - $3" | mail -s "ALERT updateServerIP.sh" $emailAddress
  fi
}

ipcheck() {
  isdiff=1
  hostlist=$2
  ipaddressnew=$(dig +short myip.opendns.com @resolver1.opendns.com)
  for host in ${hostlist[@]}; do
    if [ $host == "@" ]; then
      ipaddresscurrent=$(nslookup $1 8.8.8.8 | grep Address | grep -v "#" | cut -f2 -d" ")
    else
      ipaddresscurrent=$(nslookup $host.$1 8.8.8.8 | grep Address | grep -v "#" | cut -f2 -d" ")
    fi
    diffcheck=$(echo $ipaddresscurrent | grep -c $ipaddressnew)
    if [ $diffcheck -eq 0 ]; then
      isdiff=0
    fi
  done
}

update() {
  hostlist=$1
  for host in ${hostlist[@]}; do
    response=$(curl https://dynamicdns.park-your-domain.com/update?host=$host\&domain=$2\&password=$3\&ip=$ipaddressnew)
    errorcount=$(echo $response | perl -pe "s/.*\<errcount\>(\d+)\<\/ErrCount\>.*/\$1/gi")
    if [ $errorcount -gt 0 ]; then
      logMessage 3 1 "$response"
    else
      message="Updated IP Address $host.$2 from $ipaddresscurrent to $ipaddressnew"
      logMessage 1 0 "$message"
    fi
  done
}

# --------------------------------
# ------ BEGIN HOST DEFINITION ---
# --------------------------------
isdiff='1'
# --------------------------------
hostlist=("@")
domain="mydomain.com"
password="DynamicDnsSecretFromNamecheap"
# --------------------------------
ipcheck $domain $hostlist
if [ $isdiff -eq 0 ]; then
  update $hostlist $domain $password
fi
# --------------------------------
# -------- END HOST DEFINITION ---
# --------------------------------

exit 0

Systemd Service File

/etc/systemd/system/updateServerIP.service
[Unit]
Description=Check external IP address against DNS and update if needed

[Service]
Type=oneshot
ExecStart=/usr/local/bin/updateServerIP.sh

Systemd Timer File

/etc/systemd/system/updateServerIP.timer
[Unit]
Description=Check external IP address against DNS and update if needed

[Timer]
# Time to wait after booting before the first run
OnBootSec=15min
# Time each consecutive run
OnUnitActiveSec=30min
Unit=updateServerIP.service

[Install]
WantedBy=timers.target

DNS Propagation

Once you’ve created the service and timer files, run the commands (as root) systemctl enable updateServerIP.timer, and systemctl start updateServerIP.timer. Now the script will be run automatically 15 minutes after the VM boots up, and every 30 minutes after that.

Run the command systemctl list-timers, you should see the new one we just created and next to it the remaining time until the next run, maybe go make some coffee while you wait?

Log back into your registrar and check the DNS settings page again, hopefully that 0.0.0.0 address got updated to the correct one? If it did, now you have to wait for DNS Propagation. How long it will take depends mainly on what the time-to-live (TTL) was set to before you made the record changes which can sometimes take days, but it’s usually pretty quick for brand new domains, click the link below to check propagation for your domain.

If it’s not there yet, give it some more time. If it’s been a few days and it’s still not working, use the DNSreport from DNStools to get some more detailed information, then reach out to your registrar, they should be able to send you in the right direction if they can’t fix the issue.

Reconfigure NGINX

First we’ll need a self-signed certificate to set things up, we will get legitimate certificates from LetsEncrypt down below but we need something to get the sites running on port 443 in order to do that (Just like when you buy a pair of scissors and need a pair of scissors to open the packaging). From your NGINX VM run the following commands as root:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365000 -nodes
mv key.pem /etc/ssl/private/ssl-cert-snakeoil.key
mv cert.pem /etc/ssl/certs/ssl-cert-snakeoil.pem

The first command will ask you a handful of questions, the answers aren’t that important in this use case so don’t worry about getting it wrong.

Then run the command openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 (as root), to generate Diffie-Hellman parameters for your NGINX server, without getting into too much jargon, these are used in key exchanges and should be unique to your specific server for security, you can read more here if you are interested.

Create the file /etc/nginx/snippets/ssl-params.conf:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256: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';
ssl_prefer_server_ciphers on;
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
ssl_dhparam /etc/ssl/certs/dhparam.pem;

The Strict-Transport-Security header is commented out here because it will cause you major problems if you remove SSL/TLS from any proxied site later, you can enable it after you are sure everything is running smoothly. By enabling this option you get some security against protocol downgrade attempts.


Note: For a more compatible set of SSL protocols while maintaining a decent level of security, replace the first two lines of the ssl-params.conf file with the following.

ssl_protocols SSLv3 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256: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 EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!CAMELLIA';

Now we need to make some changes to the configuration files in the /etc/nginx/sites-available directory, here’s what I’m starting with:

server {
    listen 80;
    server_name 4t.burns.lab;
    location / {
        proxy_hide_header X-Powered-By;
        proxy_pass_header Authorization;
        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_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        client_max_body_size 0;
        proxy_read_timeout 36000s;
        proxy_redirect off;
        proxy_pass http://172.16.44.102:5000;
    }
}

Change the listen 80; line to listen 443 ssl;

Change the server_name to a fully qualified domain name (FQDN), this should be a domain you own, or a subdomain of it, followed by a space and then www. + your FQDN.

For example: server_name mysite.com www.mysite.com;, or server_name 4t.mysite.com www.4t.mysite.com;

Right after the server_name, add the following five lines. The first two are for the self-signed certificate we just created, the second two are for the LetsEncrypt certificate that we don’t yet have so they’re commented out, and the last line grabs the SSL parameters file we just made.

ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# ssl_certificate /etc/letsencrypt/live/4t.mydomain.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/4t.mydomain.com/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf;

Note that the FQDN is part of the path for the LetsEncrypt certificates (4t.mydomain.com in the example above), you should change this to your FQDN.

Add the following to the very top of the file, to redirect HTTP traffic to HTTPS:

server {
    listen 80;
    server_name 4t.mydomain.com www.4t.mydomain.com;
    return 301 https://www.4t.mydomain.com$request_uri;
}

Again, change the FQDN to your own here.

Add the following location block at the end of the server block (above the last curly brace).

# letsencrypt validation
location '/.well-known/acme-challenge' {
    default_type "text/plain";
    root /var/www/html;
}

When you request a certificate from LetsEncrypt, their server must ensure that you own the domain you are requesting a certificate for. To do that, the command line tool you use to request the certificate will get a token from LetsEncrypt and put it in a file at /var/www/html/.well-known/acme-challenge, then the LetsEncrypt server will request that file from your FQDN at mysite.com/.well-known/acme-challenge to verify the token.

You should end up with something similar to this:

server {
    listen 80;
    server_name 4t.mydomain.com www.4t.mydomain.com;
    return 301 https://4t.mydomain.com$request_uri;
}
server {
    listen 443 ssl;
    server_name 4t.mydomain.com www.4t.mydomain.com;
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
    # ssl_certificate /etc/letsencrypt/live/4t.mydomain.com/fullchain.pem;
    # ssl_certificate_key /etc/letsencrypt/live/4t.mydomain.com/privkey.pem;
    include /etc/nginx/snippets/ssl-params.conf;
    location / {
        proxy_hide_header X-Powered-By;
        proxy_pass_header Authorization;
        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_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        client_max_body_size 0;
        proxy_read_timeout 36000s;
        proxy_redirect off;
        proxy_pass http://172.16.44.102:5000;
    }
    # letsencrypt validation
    location '/.well-known/acme-challenge' {
        default_type "text/plain";
        root /var/www/html;
    }
}

Run the command nginx -t to test the configuration and make sure everything checks out, then systemctl restart nginx to load the changes.

LetsEncrypt

Still on your NGINX VM as root, run apt install -y certbot to install the Certbot tool we will use to get LetsEncrypt certificates, then do a “dry run” with the following command just to make sure everything checks out.

certbot certonly --webroot -w /var/www/html -d 4t.mydomain.com -d www.4t.mydomain.com --dry-run

You will be asked to provide your Email address for expiration alerts, and to accept the terms of service. The renewal alerts are handy because if something ever goes wrong and renewal fails, you’ll be informed before there is a real problem.

If you have errors, check the DNSreport again from DNSstuff to look for any problems.

If everything worked, run the Certbot command again but without the --dry-run at the end. Now that you have a valid certificate, go back in to /etc/nginx/sites-available/4t-app.conf to remove the two snakeoil certificate lines and uncomment the LetsEncrypt lines. One more nginx -t and systemctl restart nginx, then you’re up and running!

One last detail, run the command systemctl list-timers | grep certbot to make sure the automatic renewal timer is set up, you should see a line with certbot.timer in it.

A Note on Apex Domains

Here’s another chance for you to learn from my mistakes! You’ll notice that this website is primarily accessed as dlford.io (the apex domain), well, one of my pending to-do’s is migrating all of my sites to subdomains (e.g. www.dlford.io).

As for why, www.yes-www.org has a very thorough list of reasons.

It’s much easier to implement this from the start, so I would advise you to make the following changes to your NGINX configurations now:

  • Change the SSL redirect to www
  • Add an additional redirect from the apex domain to www
  • Take the apex domain off of the server_name directive in the main server block

Here’s a full example (Keep in mind you still need SSL certificates for both the apex domain and the www domain):

# Forward both apex and www from non-SSL to https://www
server {
    listen 80;
    server_name mydomain.com www.mydomain.com;
    return 301 https://www.mydomain.com$request_uri;
}

# Forward apex SSL to https://www
server {
    listen 443 ssl;
    server_name mydomain.com;
    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
    include /etc/nginx/snippets/ssl-params.conf;
    return 301 https://www.mydomain.com$request_uri;
}

# No apex in server_name
server {
    listen 443 ssl;
    server_name www.mydomain.com;
    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
    include /etc/nginx/snippets/ssl-params.conf;
    location / {
        proxy_hide_header X-Powered-By;
        proxy_pass_header Authorization;
        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_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        client_max_body_size 0;
        proxy_read_timeout 36000s;
        proxy_redirect off;
        proxy_pass http://172.16.44.102:5000;
    }
    # letsencrypt validation
    location '/.well-known/acme-challenge' {
        default_type "text/plain";
        root /var/www/html;
    }
}