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:
- Shut down Apache, freeing up TCP ports 80 & 443
- Rewrite the virtualhost config in nginx to listen on standard ports
- Perform the --nginx plugin challenge with stapling and redirect
- Rewrite the virtualhost config in nginx to listen on non-standard ports once again
- Rewrite the virtualhost config to have SSL listen on non-standard port
- Restart nginx
- 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}"'