Renewing certificates for webserver running on non-standard TCP port using renewal-hooks

I have nginx running on a dev machine alongside apache. Apache is bound to a routable IP and listens on TCP port 80 and 443.

My web server is (include version): nginx/1.18.0 (Ubuntu)

The operating system my web server runs on is (include version): Ubuntu 22.04.5 LTS

The version of my client is (e.g. output of certbot --version or certbot-auto --version if you're using Certbot): certbot 3.0.1

No help needed. Just wanted to share my solution for renewing SSL certificates for an instance of nginx that is bound to non-standard ports.

There are alternate challenges that could be used, but I find them yucky, and wanted to use the --nginx plugin.

In this particular dev environment, I only have one external IP address for it to use for two webservers. So while Apache listens on TCP ports 80 and 443, we have the nginx instance listening on 16680 and 16643.

Certbot won't be able to perform the --nginx plugin challenge on those ports, so I used renewal-hooks to:

  1. Shut down Apache, freeing up TCP ports 80 & 443
  2. Rewrite the virtualhost config in nginx to listen on standard ports
  3. Perform the --nginx plugin challenge with stapling and redirect
  4. Rewrite the virtualhost config in nginx to listen on non-standard ports once again
  5. Rewrite the virtualhost config to have SSL listen on non-standard port
  6. Restart nginx
  7. Restart apache
#!/usr/bin/env bash

# Assign variables
HOSTNAME="$1"
EXTERNAL_IP="$2"
ORIGINAL_PORT="$3"
NEW_PORT="$4"
SITES_AVAILABLE="/etc/nginx/sites-available"
SITES_ENABLED="/etc/nginx/sites-enabled"
SITE_SOURCE="../sites-available"                                                                                                                                                                                                                                                    

# Function for error handling
error_exit() {
  echo "$1"
  exit 1
}

# Check if all operands are provided
for arg in "$HOSTNAME" "$EXTERNAL_IP" "$ORIGINAL_PORT" "$NEW_PORT"; do
  if [ -z "$arg" ]; then
    error_exit "Usage: $0 <hostname> <external-ip> <original-http-port> <new-http-port>"
  fi
done

# Check if the virtual host configuration file exists
if [ ! -f "$SITES_AVAILABLE/$HOSTNAME" ] && [ ! -f "$SITES_AVAILABLE/$HOSTNAME.conf" ]; then
  error_exit "Error: Virtual host configuration file does not exist in $SITES_AVAILABLE/"
fi

# Check for redundancy (both $HOSTNAME and $HOSTNAME.conf existing)
if [ -f "$SITES_AVAILABLE/$HOSTNAME" ] && [ -f "$SITES_AVAILABLE/$HOSTNAME.conf" ]; then
  error_exit "Conflict: Both $HOSTNAME and $HOSTNAME.conf exist in $SITES_AVAILABLE/."
fi

# Assign SITE_CONF to the existing configuration file
if [ -f "$SITES_AVAILABLE/$HOSTNAME" ]; then
  SITE_CONF="$SITES_AVAILABLE/$HOSTNAME"
elif [ -f "$SITES_AVAILABLE/$HOSTNAME.conf" ]; then
  SITE_CONF="$SITES_AVAILABLE/$HOSTNAME.conf"
fi

# Check if there is a symbolic link in $SITES_ENABLED pointing to $SITE_SOURCE/$HOSTNAME
if [ ! -L "$SITES_ENABLED/$HOSTNAME" ] && [ ! -L "$SITES_ENABLED/$HOSTNAME.conf" ]; then
  error_exit "$HOSTNAME not enabled."
fi

# Step 1: Check the $SITE_CONF file for listen directives
LISTEN_DIRECTIVES=$(grep -E "listen\s+" "$SITE_CONF")

# Step 2: Check if any listen directive contains $EXTERNAL_IP
if ! echo "$LISTEN_DIRECTIVES" | grep -q "$EXTERNAL_IP"; then
  error_exit "Error: $HOSTNAME not bound to $EXTERNAL_IP."
fi

# Step 3: Find the listen directives that contain $ORIGINAL_PORT
ORIGINAL_PORT_DIRECTIVES=$(echo "$LISTEN_DIRECTIVES" | grep "$ORIGINAL_PORT")

# Step 4: If it contains $EXTERNAL_IP, change $ORIGINAL_PORT to $NEW_PORT
if echo "$ORIGINAL_PORT_DIRECTIVES" | grep -q "$EXTERNAL_IP"; then
  sed -i "s/listen $EXTERNAL_IP:$ORIGINAL_PORT/listen $EXTERNAL_IP:$NEW_PORT/g" "$SITE_CONF"
fi

# Step 5: Replace 'listen 443' with 'listen $EXTERNAL_IP:$NEW_PORT'
sed -i "s/listen $ORIGINAL_PORT/listen $EXTERNAL_IP:$NEW_PORT/g" "$SITE_CONF"

# Step 6: Specify new HTTPS port in redirection directive
sed -i '/301/s/\(https:\/\/\$host\)\1/:$NEW_PORT/' "$SITE_CONF"

exit 0

renewal-hooks:

pre-hook: /etc/letsencrypt/renewal-hooks/pre/

#!/usr/bin/env bash

# pre-hook

CERT_HOSTNAME=$(basename "$0")

systemctl stop apache2
ngnsleport $CERT_HOSTNAME <external-IP> 16680 80
systemctl reload nginx

deploy-hook: /etc/letsencrypt/renewal-hooks/deploy/

#!/usr/bin/env bash

# deploy-hook

CERT_HOSTNAME=$(basename "$0")

ngnsleport $CERT_HOSTNAME <external-IP> 443 16643
ngnsleport $CERT_HOSTNAME <external-IP> 80 16680
systemctl restart nginx

post-hook: /etc/letsencrypt/renewal-hooks/post/

#!/usr/bin/env bash

# post-hook

# CERT_HOSTNAME=$(basename "$0")

systemctl start apache2

renew config: /etc/letsencrypt/renew/.conf

# renew_before_expiry = 30 days
version = 3.0.1
archive_dir = /etc/letsencrypt/archive/<virtualhost-hostname>
cert = /etc/letsencrypt/live/<virtualhost-hostname>/cert.pem
privkey = /etc/letsencrypt/live/<virtualhost-hostname>/privkey.pem
chain = /etc/letsencrypt/live/<virtualhost-hostname>/chain.pem
fullchain = /etc/letsencrypt/live/<virtualhost-hostname>/fullchain.pem

# Options used in the renewal process
[renewalparams]
account = e9ec97a53fa4b2c159aa8094916dc87a
authenticator = nginx
installer = nginx
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa
pre_hook = /etc/letsencrypt/renewal-hooks/pre/<virtualhost-hostname>
deploy_hook = /etc/letsencrypt/renewal-hooks/deploy/<virtualhost-hostname>
post_hook = /etc/letsencrypt/renewal-hooks/post/<virtualhost-hostname>

command/cronjob:

certbot --nginx --staple-ocsp --redirect -d <virtualhost-hostname>

you can confirm the TCP port binding changes by running in another window:

watch -n 0.1 'ss -lpn | grep -E ":80|:443|:16680|:16643" | awk "{print \$5, \$7}" | awk -F "," "{print \$1}"'

Thank you for sharing your solution to this specific issue!

I'm wondering though: wouldn't it be a little bit simpler to just have Apache also configured as a reverse proxy for nginx? There would need to be differentiating info required, like separate hostnames or a specific path.. But it would mean you could use the webroot plugin (perhaps a 'shared' webroot between the main Apache configuration and the Apache reverse proxy configuration). Using a reverse proxy, you might not even need to connect to those ports 16680/16643 and let Apache do all the certificate stuff.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.