GitHub picture of ramblehead

SSL Certificates with DNS Wild Wild Card

Self-hosted wildcard DNS configuration guide with multiple servers on a single IP with similar port numbers (e.g. multiple https on different subdomains) and SSL termination with a single certificate for all hosted servers.

Priming

This configuration guide was tested on Raspberry Pi OS (64-bit), but should be compatible with any Debian-based Linux.

For serving multi-tenant self-hosted applications with a single SSL certificate one might use a DNS provider that supports the following features:

Dynamic DNS configuration process has been described in Dynamic DNS article.

Wildcard Subdomain Address and DNS-01 challenge configurations are closely related subjects. Therefore their configurations are both described in the current article.

However Google Domains is used here as an example, for the new deployments Google Domains may not be used as it will no longer offer new domain registrations.

NGINX is used as a reverse proxy and as SSL termination.
LEGO is used for obtaining SSL certificates and keys.
systemd timer is used to automate SSL certificates and keys renewals before they expire.

For simplicity it is assumed that all software are running on the same host (Raspberry Pi 4 in my case).

Mock web-server and error page

For the purpose of this article, it is assumed that that the host is already running a web-server such as Next.js at the following address:

http://localhost:3000/

It is also assumed that there is a static HTML page on the host file system that holds page not found error. For example:

/home/rh/artizanya-host/packages/wui-errors/out/404.html

(I might write another article on setting-up Next.js 14 as a web server and as a static pages generator if there is a need for such guidelines.)

Configuring Router Port Forwarding

The configuration of port forwarding depends on the Internet provider and the router in use. In this article, it is assumed that ports 80 and 443 are forwarded to the corresponding ports at the internal IP address running the NGINX server.

Installing and Configuring LEGO

Refer to your DNS provider documentation and to LEGO DNS Providers documentation to configure wildcard subdomain addresses and to obtain API key for DNS-01 challenge.

At the time of writing, LEGO is relatively new, and the versions included with most Linux distributions are not up-to-date with the current release. The best way to obtain the latest version is to compile it from the source. LEGO is written in Golang and requires a fairly recent version of Golang, which might not be available in a Linux distribution.

To compile LEGO from the source, Golang can be installed from its original binary distribution by following the official instructions.

With Golang installed, LEGO can be built and installed using the following command:

$ # go1.17+
$ # environment variable: GO111MODULE=on
$ go install github.com/go-acme/lego/v4/cmd/lego@latest

Getting SSL Keys and Certificates Using LEGO

The following command is an example of how LEGO may be used for DNS-01 challenge and wildcard subdomain:

$ # environment variable: GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
$ cd /home/rh/artizanya-host
$ lego --accept-tos \
       --dns=googledomains \
       --domains='artizanya.com' \
       --domains='*.artizanya.com' \
       --email='[email protected]' \
       run

Getting NGINX files required for Let's Encrypt SSL

The following commands could be used to obtain NGINX files required for Let's Encrypt SSL:

$ cd /home/rh/artizanya-host
$ mkdir -pv nginx/letsencrypt && cd nginx/letsencrypt
$ wget --backups=1 https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf
$ wget --backups=1 https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem

Installing and Configuring NGINX

NGINX configuration file for artizanya.com:

# /home/rh/artizanya-host/nginx/sites-available/artizanya.com

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  server_name *.artizanya.com;

  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl default_server;
  listen [::]:443 ssl default_server;

  ssl_certificate /home/rh/artizanya-host/.lego/certificates/artizanya.com.crt;
  ssl_certificate_key /home/rh/artizanya-host/.lego/certificates/artizanya.com.key;
  ssl_trusted_certificate /home/rh/artizanya-host/.lego/certificates/artizanya.com.issuer.crt;
  include /home/rh/artizanya-host/nginx/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /home/rh/artizanya-host/nginx/letsencrypt/ssl-dhparams.pem;

  server_name *.artizanya.com;

  root /home/rh/artizanya-host/packages/wui-errors/out;
  index 404.html;
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate /home/rh/artizanya-host/.lego/certificates/artizanya.com.crt;
  ssl_certificate_key /home/rh/artizanya-host/.lego/certificates/artizanya.com.key;
  ssl_trusted_certificate /home/rh/artizanya-host/.lego/certificates/artizanya.com.issuer.crt;
  include /home/rh/artizanya-host/nginx/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /home/rh/artizanya-host/nginx/letsencrypt/ssl-dhparams.pem;

  server_name artizanya.com;

  location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

To enable above NGINX configuration:

$ ln -s /home/rh/artizanya-host/nginx/sites-available/artizanya.com /etc/nginx/sites-enabled

The following command can be used to verify NGINX configuration:

# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

To reload NGINX after configuration change:

# systemctl reload nginx

To view NGINX available output:

$ journalctl -u dns01_challenge.service

To follow NGINX current output:

$ journalctl --no-pager --follow -u nginx

systemd LEGO auto-update timer

The following bash, service and timer files are required to execute LEGO on systemd timer:

# /home/rh/artizanya-host/utils/dns01_challenge.sh

cd ..

ACTION=run
if [ -f .lego/certificates/artizanya.com.crt ]; then
  ACTION=renew
fi

lego --accept-tos \
     --dns=googledomains \
     --domains='artizanya.com' \
     --domains='*.artizanya.com' \
     --email='[email protected]' \
     "${ACTION}"

systemctl reload nginx
# /home/rh/artizanya-host/systemd/dns01_challenge.timer

[Unit]
Description=Run Artizanya dns-01_challenge.service every 2 months

[Timer]
Unit=dns01_challenge.service
# Run on the 7th day of every odd month
OnCalendar=*-01,03,05,07,09,11-07 00:00:00 UTC
# or run every 2 months
# OnCalendar=*-*-1/2 00:00:00 UTC
Persistent=true

[Install]
WantedBy=timers.target
# /home/rh/artizanya-host/systemd/dns01_challenge.service

[Unit]
Description=Execute dns01_challenge.sh script

[Service]
Type=oneshot
WorkingDirectory=/home/rh/artizanya-host
EnvironmentFile=/home/rh/artizanya-host/.env
Environment=PATH=/home/rh/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/home/rh/artizanya-host/utils/dns01_challenge.sh

.env file referred by service option EnvironmentFile should contain environment variable GOOGLE_DOMAINS_ACCESS_TOKEN (see LEGO DNS Providers documentation for the environment variables required by LEGO for your DNS provider).

To enable above service and timer:

# systemctl enable /home/rh/artizanya-host/systemd/dns01_challenge.service
# systemctl enable --now /home/rh/artizanya-host/systemd/dns01_challenge.timer

To make timer targets to wait until real time is initialised on systems with no real time clock (such as Raspberry Pi 4):

# systemctl enable --now systemd-time-wait-sync.service

To reload systemd after changing enabled service and timer files:

# systemctl daemon-reload

DANE TLSA records

To print TLSA record for the current TSL certificate:

$ printf '_25._tcp.%s. IN TLSA 3 1 1 %s\n' artizanya.com \
  (openssl x509 -in /home/rh/artizanya-host/.lego/certificates/artizanya.com.crt \
   -noout -pubkey | \
   openssl pkey -pubin -outform DER | \
   openssl dgst -sha256 -binary | \
   hexdump -ve '/1 "%02x"')

The output of the above command should be entered to DNS records (I do not know how to automate this step yet).

References