Setting Up Wildcard Subdomains with SSL on a Debian Application

From Delft Solutions
Revision as of 06:04, 31 October 2024 by Alois (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

This guide provides a step-by-step approach to setting up wildcard subdomains with SSL on a Debian-based application. Wildcard subdomains allow applications to dynamically support multiple subdomains (abc.example.com, xyz.example.com) under a single SSL certificate.

In this guide, the focus is on configuring a Debian package to handle wildcard subdomains primarily through updates to the debian/postinst file. The guide uses the kaboom-api app as an example, with staging-elearning.nl as the domain name on which we’re setting up wildcard subdomains.

If you do not need or prefer not to modify the application code itself, you can still follow the key steps and commands described in this guide directly from the terminal. This will allow you to set up SSL for wildcard subdomains without diving into the application’s Debian packaging configuration.

By the end of this guide, you will have a fully automated process for configuring and renewing SSL certificates for wildcard subdomains, leveraging tools like Certbot and DNS authentication.

Prerequisites

1. DNS Configuration for Wildcard Subdomains

Access your DNS repository and add the necessary A and AAAA records for the wildcard subdomain you plan to use. This typically involves adding entries like *.example.com pointing to your server’s IP address.

The following is an example of what has been done for the domain name staging-elearning.nl

staging-elearning.nl.zone

$ORIGIN staging-elearning.nl.
$TTL 3600
@	IN	3600	SOA	delftsolutions.ns1.signaldomain.nl.	info.signaldomain.nl. (
		<serial>	; don't modify, auto incremented
		86400		; secondary refresh
		7200		; secondary retry
		3600000		; secondary expiry
		600			; negative response ttl
	)

@	3600	IN	NS	ns2.signaldomain.net.
@	3600	IN	NS	delftsolutions.ns1.signaldomain.nl.

*   IN  3600 A     193.5.147.172
*   IN  3600 AAAA  2a0c:8187:0:201::196

2. Required Packages

  • python3-certbot-dns-rfc2136
  • openssl
  • nginx
  • dnsutils
  • certbot

Configuring SSL and Wildcard Subdomains

1. Handling Environment Variables

We’ll first add three environment variables to capture essential information: DNS_AUTHENTICATION, CERTBOT_EMAIL, and FQDN. These variables will be defined using Debconf, which allows us to prompt for values during installation and configuration.

DNS_AUTHENTICATION: This string is required for Certbot’s DNS-based challenge verification. The format includes a keyname, algorithm, and secret for authentication, followed by the authoritative DNS hostname. This string is currently obtained using the signal domain api package running the command : signaldomain-api key certbot create <domain_name>

The expected format is: dns://<key_name>:<key_algorithm>~<key_secret_base64>@<authoritive_nameserver_domainname>

example: dns://staging-elearning_nl__certbot._keys.delftsolutions.signaldomain._internal.usersignal.nl.:hmac-sha256~<key_secret>@ns1.signaldomain.nl/staging-elearning_nl__certbot._keys.delftsolutions.signaldomain._internal.usersignal.nl.

CERTBOT_EMAIL: This email address is used when registering an account with Let’s Encrypt. Important notifications about certificate issues will be sent to this address.

FQDN: This is the fully qualified domain name of the primary domain for which wildcard SSL certificates will be issued. staging-elearning.nl for this guide example.


Here’s how to set these variables in debian/templates, define prompts in debian/config, and retrieve them in debian/postinst.

in debian/templates, add the following entries to create prompt templates for each variable:

…
Template: <PKG_NAME>/DNS_AUTHENTICATION
Type: string
Default:
Description: DNS authentication string in the following format: dns://<key_name>:<key_algorithm>~<key_secret_base64>@<authoritative_nameserver_domainname>

Template: <PKG_NAME>/CERTBOT_EMAIL
Type: string
Default:
Description: Enter the email that certificate issues should be reported to. Entering this will result in accepting the Let's Encrypt terms and conditions.

Template: <PKG_NAME>/FQDN
Type: string
Default:
Description: Enter the fully qualified domain name for ...
…

<PKG_NAME> is the name of your Debian package, kaboom-api in our case.

Add the following lines to your debian/config file to prompt for these variables during configuration: in debian/templates, add the following entries to create prompt templates for each variable:

…
db_input medium <PKG_NAME>/CERTBOT_EMAIL || true
db_input medium <PKG_NAME>/DNS_AUTHENTICATION || true
db_input medium <PKG_NAME>/FQDN || true
…

In debian/postinst, retrieve the stored values with the following lines:

…
db_get <PKG_NAME>/DNS_AUTHENTICATION
DNS_AUTHENTICATION="$RET"

db_get <PKG_NAME>/CERTBOT_EMAIL
CERTBOT_EMAIL="$RET"

db_get <PKG_NAME>/FQDN
FQDN="$RET"
…


These values shall be set or updated once the whole config is over, by running the following command: sudo dpkg-reconfigure <PKG_NAME>

2. Automating SSL and Wildcard Domain Setup in postinst

Here we will break down concern by concern how to configure the debian/postinst file.

a. Creating the dns-auth.conf File

The dns-auth.conf file will be generated from the DNS_AUTHENTICATION variable, which contains the details for Certbot’s DNS challenge configuration. Add the following to the debian/postinst file to create this file:

dns_hostname_path="$(cut -d'@' -f2- <<<"$DNS_AUTHENTICATION")"
dns_schema_auth="$(cut -d'@' -f1 <<<"$DNS_AUTHENTICATION")"

dns_hostname="$(cut -d'/' -f1 <<<"$dns_hostname_path")"
dns_auth="$(cut -d'/' -f3- <<<"$dns_schema_auth")"
dns_auth_keyname="$(cut -d':' -f1 <<<"$dns_auth")"
dns_auth_algorithm="$(cut -d':' -f2- <<<"$dns_auth" | cut -d'~' -f1 | tr '[:lower:]' '[:upper:]')"
dns_auth_secret="$(cut -d':' -f2- <<<"$dns_auth" | cut -d'~' -f2-)"

dns_host_aaaa="$(dig +short AAAA "$dns_hostname" | head -n1)"

[ -d etc/<DNS_CONF_DIR> ] || mkdir -p etc/<DNS_CONF_DIR>

umask 266
cat > etc/<DNS_CONF_DIR>/dns-auth.conf <<CONF
# Managed by apt, please use dpkg-reconfigure <PKG_NAME> to modify
dns_rfc2136_server = $dns_host_aaaa
dns_rfc2136_port = 53
dns_rfc2136_name = $dns_auth_keyname
dns_rfc2136_secret = $dns_auth_secret
dns_rfc2136_algorithm = $dns_auth_algorithm
CONF
umask 022

This configuration file will be used by Certbot to authenticate and verify domain ownership via DNS challenges. In the case of our guide with the kaboom-api example, <DNS_CONF_DIR> is kaboom, it's up to you to select the right naming for your case.

Once the script has been executed The dns-auth.conf file should look something like this:

dns_rfc2136_server = 2a0c:8187::120
dns_rfc2136_port = 53
dns_rfc2136_name = staging-elearning_nl__certbot._keys.delftsolutions.signaldomain._internal.usersignal.nl.
dns_rfc2136_secret = <secret-key>
dns_rfc2136_algorithm = HMAC-SHA256 

Make sure that proper letter case is observed as this would cause the script to fail with unclear error messages.

b. Setting Up Certbot and Requesting Certificates

To handle SSL certificates, Certbot needs to register an account (if not already registered) and request a certificate for the primary domain and wildcard subdomain. Add the following to postinst to check and register Certbot, then request the certificate:

certbot_account_count="$(find /etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory/ -maxdepth 1 -mindepth 1 | wc -l)"
if [ "z$certbot_account_count" = "z0" ]; then
    certbot register --non-interactive --email "$CERTBOT_EMAIL" --no-eff-email --agree-tos
fi

[ ! -f "/etc/letsencrypt/live/<CERT_NAME>/fullchain.pem" ] || certbot certonly --non-interactive --cert-name <CERT_NAME> --dns-rfc2136 --dns-rfc2136-credentials etc/<DNS_CONF_DIR>/dns-auth.conf --domain "$FQDN" --domain "*.$FQDN" --deploy-hook /usr/share/<PKG_NAME>/bin/cert-deploy

This block registers Certbot, checks for an existing certificate, and if none exists, requests a new certificate using DNS authentication with the specified dns-auth.conf file. The --deploy-hook option calls the cert-deploy file after each certificate issuance or renewal. We will create the cert-deploy in step f.

In the case of our guide with the kaboom-api example, <CERT_NAME> is kaboom-elearning, again it's up to you to select the right naming for your case.

c. Generating Diffie-Hellman Parameters for SSL

Diffie-Hellman parameters enhance SSL security. To ensure this file exists, add the following to debian/postinst:

[ -f "etc/<DNS_CONF_DIR>/ssl-dhparams.pem" ] || openssl dhparam -out etc/<DNS_CONF_DIR>/ssl-dhparams.pem 2048

This code checks for an existing ssl-dhparams.pem file, generating one if it doesn’t exist, using 2048-bit encryption for security.

d. Configuring Nginx for Wildcard SSL

Finally, configure Nginx to handle requests for the wildcard domain and apply SSL settings. Here’s the code to create a new Nginx server block for the wildcard domain:

cat >/etc/nginx/sites-available/<CERT_NAME> <<CONF
server {
	root /usr/share/<PKG_NAME>/public;
	server_name *.$FQDN;

	location / {
    	proxy_pass http://<APP_UPSTREAM>/;
    	proxy_set_header Host \$host;
    	proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    	proxy_set_header X-Forwarded-Proto \$scheme;
    	proxy_buffers 8 32k;
    	proxy_buffer_size 64k;
    	client_max_body_size 0;
    	proxy_redirect off;
    	proxy_buffering off;
	}

	listen [::]:443 ssl http2;
	listen 443 ssl http2;

	ssl_certificate /etc/letsencrypt/live/<CERT_NAME>/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/<CERT_NAME>/privkey.pem;
	ssl_dhparam etc/<DNS_CONF_DIR>/ssl-dhparams.pem;

	ssl_session_cache shared:le_nginx_SSL:10m;
	ssl_session_timeout 1440m;
	ssl_session_tickets off;

	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_prefer_server_ciphers off;
	ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
}
CONF

[ -L /etc/nginx/sites-enabled/<CERT_NAME> ] || ln -s /etc/nginx/sites-available/<CERT_NAME> /etc/nginx/sites-enabled

nginx -q -t && service nginx reload

In the case of our guide with the kaboom-api example, <APP_UPSTREAM> is kaboom-api, it refers to the backend application server(s) receiving proxied requests.

e. Wrapping everything in an if statement

You do not want to run that part of the postinst script if you do not have the DNS_AUTHENTICATION, CERTBOT_EMAIL, and FQDN variables set.

This is why we’ll wrap everything we just covered inside an if statement

if [ -n "$DNS_AUTHENTICATION" ] && [ -n "$CERTBOT_EMAIL" ] && [ -n "$FQDN" ] ; then
  #Everything we just wrote
else
  echo "one or more of DNS_AUTHENTICATION, CERTBOT_EMAIL, FQDN are missing, skipping  wildcard subdomains SSL certificate setup."
fi

Here is what the finished code looks like in the kaboom-api example

if [ -n "$DNS_AUTHENTICATION" ] && [ -n "$CERTBOT_EMAIL" ] && [ -n "$FQDN" ] ; then

	dns_hostname_path="$(cut -d'@' -f2- <<<"$DNS_AUTHENTICATION")"
	dns_schema_auth="$(cut -d'@' -f1 <<<"$DNS_AUTHENTICATION")"

	dns_hostname="$(cut -d'/' -f1 <<<"$dns_hostname_path")"

	dns_auth="$(cut -d'/' -f3- <<<"$dns_schema_auth")"
	dns_auth_keyname="$(cut -d':' -f1 <<<"$dns_auth")"
	dns_auth_algorithm="$(cut -d':' -f2- <<<"$dns_auth" | cut -d'~' -f1 | tr '[:lower:]' '[:upper:]')"
	dns_auth_secret="$(cut -d':' -f2- <<<"$dns_auth" | cut -d'~' -f2-)"

	dns_host_aaaa="$(dig +short AAAA "$dns_hostname" | head -n1)"

	[ -d /etc/kaboom ] || mkdir -p /etc/kaboom

	umask 266
	cat >/etc/kaboom/dns-auth.conf <<CONF
# Managed by apt, please use dpkg-reconfigure kaboom-api to modify
dns_rfc2136_server = $dns_host_aaaa
dns_rfc2136_port = 53
dns_rfc2136_name = $dns_auth_keyname
dns_rfc2136_secret = $dns_auth_secret
dns_rfc2136_algorithm = $dns_auth_algorithm
CONF
	umask 022

	certbot_account_count="$(find /etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory/ -maxdepth 1 -mindepth 1 | wc -l)"
	if [ "z$certbot_account_count" = "z0" ]; then
    	certbot register --non-interactive --email "$CERTBOT_EMAIL" --no-eff-email --agree-tos
	fi

	echo "Checking if SSL certificate already exists"
	if [ ! -f "/etc/letsencrypt/live/kaboom-elearning/fullchain.pem" ]; then
            	echo "Requesting new certificate for $FQDN and *.$FQDN"
            	certbot certonly --non-interactive --cert-name kaboom-elearning --dns-rfc2136 --dns-rfc2136-credentials /etc/kaboom/dns-auth.conf --domain "$FQDN" --domain "*.$FQDN" --deploy-hook /usr/share/kaboom-api/bin/cert-deploy
    	if [ $? -eq 0 ]; then
        	echo "Certificate obtained successfully"
    	else
        	echo "Error obtaining certificate"
    	fi
	else
    	echo "Certificate already exists"
	fi

	echo "Checking if SSL DHParams file already exists"
	if [ ! -f "/etc/kaboom/ssl-dhparams.pem" ]; then
            	openssl dhparam -out /etc/kaboom/ssl-dhparams.pem 2048
    	if [ $? -eq 0 ]; then
        	echo "DHParams generated successfully"
    	else
        	echo "Error generating DHParams"
    	fi
	else
    	echo "DHParams file already exists"
	fi

	cat >/etc/nginx/sites-available/kaboom-elearning <<CONF
server {
	root /usr/share/kaboom-api/public;
	server_name *.$FQDN;

	location / {
        	proxy_pass http://kaboom_api/;
        	proxy_set_header Host \$host;
        	proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        	proxy_set_header X-Forwarded-Proto \$scheme;
        	proxy_buffers 8 32k;
        	proxy_buffer_size 64k;
        	client_max_body_size 0;
        	proxy_redirect off;
        	proxy_buffering off;
	}

	listen [::]:443 ssl http2;
	listen 443 ssl http2;

	ssl_certificate /etc/letsencrypt/live/kaboom-elearning/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/kaboom-elearning/privkey.pem;
	ssl_dhparam /etc/kaboom/ssl-dhparams.pem;

	ssl_session_cache shared:le_nginx_SSL:10m;
	ssl_session_timeout 1440m;
	ssl_session_tickets off;

	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_prefer_server_ciphers off;
	ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
}
CONF

	[ -L /etc/nginx/sites-enabled/kaboom-elearning ] || ln -s /etc/nginx/sites-available/kaboom-elearning /etc/nginx/sites-enabled

	nginx -q -t && service nginx reload

else
	echo "one or more of DNS_AUTHENTICATION, CERTBOT_EMAIL, FQDN are missing, skipping wildcard subdomains SSL certificate setup."
fi



f. Creating the cert-deploy Deploy Hook

The certbot command calls in a cert-deploy file via the --deploy-hook flag. This cert-deploy script, should be created in /usr/share/<PKG_NAME>/bin, and runs after each certificate issuance or renewal.

#!/bin/bash

set -euo pipefail

if [ "z$RENEWED_LINEAGE" != "z/etc/letsencrypt/live/<CERT_NAME>" ]; then
    echo "Unknown certificate renewed, ignoring" 1>&2
    exit 1
fi

nginx -q -t && service nginx reload

g. Final Step: Applying Configuration Changes

To complete the setup and ensure all configurations take effect, we need to set the required variables (DNS_AUTHENTICATION, CERTBOT_EMAIL, FQDN), run the following command: sudo dpkg-reconfigure <PKG_NAME>