Applications Azure

NGINX with SSL Certbot on Ubuntu 24.04 on Azure User Guide

| Product: NGINX with SSL Certbot on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of NGINX with SSL Certbot on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. The image ships NGINX 1.30 from the official nginx.org APT repository (the mainline branch — newer than the Ubuntu noble main branch's 1.24) plus Certbot 2.9 with the python3-certbot-nginx plugin so customers go from az vm create to a real production-grade Let's Encrypt certificate by running one command.

On every fresh customer virtual machine, NGINX is already serving an HTTPS default server on TCP 443 with a per-VM self-signed fallback cert at /etc/ssl/cloudimg/fallback.crt. The HTTP default server on TCP 80 has the ACME HTTP-01 challenge location pre-configured at /.well-known/acme-challenge/, so when the customer runs sudo certbot --nginx -d <domain>, the cert issuance and the vhost rewrite happen in a single step with no manual editing of NGINX config files.

What is included:

  • NGINX 1.30 (mainline) installed from the official nginx.org APT repository

  • Certbot 2.9 + python3-certbot-nginx plugin (the plugin auto-edits the NGINX vhost during cert issuance)

  • certbot.timer enabled (twice daily renewal check)

  • HTTP default server on TCP 80 with ACME HTTP-01 location pre-configured

  • HTTPS default server on TCP 443 with HTTP/2 enabled

  • Per-VM self-signed RSA TLS certificate at /etc/ssl/cloudimg/fallback.crt (CN nginx-cloudimg-fallback, valid 10 years) — used until customers swap to a real Let's Encrypt cert

  • Mozilla Intermediate SSL profile loaded from /etc/nginx/snippets/ssl-params.conf: TLS 1.2 + 1.3, modern cipher list, session-tickets off, HSTS header (max-age=63072000; includeSubDomains), X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN

  • Document root at /var/www/html serving a default cloudimg landing page

  • Renewal hook directories pre-created at /etc/letsencrypt/renewal-hooks/{pre,post,deploy}/

  • Ubuntu 24.04 LTS base with the latest security patches applied at build time

  • Azure Linux Agent for seamless cloud integration and SSH key injection

  • 24/7 cloudimg support with guaranteed 24-hour response SLA

Prerequisites

  • An active Azure subscription

  • A subscription to the NGINX with SSL Certbot on Ubuntu 24.04 listing on Azure Marketplace

  • An SSH public key for VM authentication

  • A virtual network and subnet in the target region

  • A registered domain — required for the Certbot HTTP-01 challenge to succeed

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for development and small production workloads. Scale up for high-traffic sites.

Step 1: Deploy from the Azure Portal

Navigate to Marketplace in the Azure Portal, search for NGINX SSL Certbot, select the cloudimg publisher entry, and click Create.

On the Networking tab attach a network security group that allows inbound TCP 22 from your management IP range, TCP 80 from the public internet (REQUIRED — Certbot uses port 80 for the HTTP-01 challenge during initial issuance and every renewal), and TCP 443 from your client networks.

Click Review + create, wait for validation, then Create. Deployment takes around two minutes.

Step 2: Deploy from the Azure CLI

RG="nginx-prod"
LOCATION="eastus"
VM_NAME="nginx-01"
ADMIN_USER="azureuser"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/nginx-ssl-certbot-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"

az group create --name "$RG" --location "$LOCATION"

az network vnet create \
  --resource-group "$RG" \
  --name nginx-vnet --address-prefix 10.94.0.0/16 \
  --subnet-name nginx-subnet --subnet-prefix 10.94.1.0/24

az network nsg create --resource-group "$RG" --name nginx-nsg

az network nsg rule create \
  --resource-group "$RG" --nsg-name nginx-nsg \
  --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" \
  --destination-port-ranges 22 --access Allow --protocol Tcp

az network nsg rule create \
  --resource-group "$RG" --nsg-name nginx-nsg \
  --name allow-http --priority 110 \
  --source-address-prefixes Internet \
  --destination-port-ranges 80 --access Allow --protocol Tcp

az network nsg rule create \
  --resource-group "$RG" --nsg-name nginx-nsg \
  --name allow-https --priority 120 \
  --source-address-prefixes Internet \
  --destination-port-ranges 443 --access Allow --protocol Tcp

az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username "$ADMIN_USER" --ssh-key-values "$SSH_KEY" \
  --vnet-name nginx-vnet --subnet nginx-subnet --nsg nginx-nsg \
  --public-ip-sku Standard

Step 3: Connect via SSH

ssh azureuser@<vm-ip>

nginx.service and certbot.timer are already running on first boot.

Step 4: Verify the NGINX + Certbot Setup

sudo systemctl status nginx --no-pager

Expected: active (running). NGINX is serving HTTP on port 80 and HTTPS on port 443 with the per-VM fallback cert.

Check the versions:

nginx -v
certbot --version
dpkg -l | grep -E '^ii.*(nginx|certbot)' | awk '{print $2,$3}'

Expected output:

nginx version: nginx/1.30.0
certbot 2.9.0
certbot                 2.9.0-1
nginx                   1.30.0-1~noble
python3-certbot         2.9.0-1
python3-certbot-nginx   2.9.0-1

Confirm the renewal timer is enabled:

sudo systemctl status certbot.timer --no-pager

Expected: Active: active (waiting) with Trigger: set to a time within the next 12 hours. The timer runs twice daily and renews any cert that is within 30 days of expiry.

Check the listening ports:

sudo ss -tln | grep -E ':80|:443'

Expected: both :80 and :443 LISTEN on 0.0.0.0 and [::].

Step 5: Issue a Real Let's Encrypt Certificate

The image ships with a self-signed fallback cert so HTTPS works immediately, but browsers will show a certificate warning until you swap it for a real Let's Encrypt cert. The swap is one command.

Pre-flight checklist:

  1. Point a DNS A record for your domain at the VM's public IP. Wait for DNS propagation (usually under five minutes; verify with dig +short <domain>).
  2. Confirm NSG rules permit inbound port 80 from the public internet — Certbot's HTTP-01 challenge requires it.
  3. Have an admin email ready for Let's Encrypt expiry notifications.

Run Certbot:

sudo certbot --nginx \
  -d example.com \
  -m admin@example.com \
  --agree-tos --no-eff-email --redirect

What this does:

  • Verifies domain ownership via the HTTP-01 challenge over port 80.
  • Requests a 90-day RSA cert from Let's Encrypt.
  • Edits /etc/nginx/conf.d/default.conf — replaces the ssl_certificate and ssl_certificate_key paths from /etc/ssl/cloudimg/fallback.* to the new /etc/letsencrypt/live/<domain>/fullchain.pem + privkey.pem.
  • Adds an HTTP→HTTPS redirect (because --redirect was passed).
  • Reloads NGINX.
  • Writes a renewal config under /etc/letsencrypt/renewal/<domain>.conf.

From here certbot.timer takes over and renews silently in the background.

Step 6: Verify HTTPS with the Real Cert

Confirm the new cert is served:

curl -sI https://<domain>/ | head -5

The response should include the new server header. Inspect the cert:

echo | openssl s_client -servername <domain> -connect <domain>:443 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

The issuer should now be Let's Encrypt (O = Let's Encrypt) and notAfter should be ~90 days out.

Step 7: Confirm Auto-Renewal

Dry-run the renewal flow without contacting the live ACME server:

sudo certbot renew --dry-run

A successful dry run prints Congratulations, all renewals succeeded. The same flow runs unattended via certbot.timer twice daily; any cert ≤ 30 days from expiry is renewed automatically.

Step 8: Hardening Headers (Pre-Configured)

The Mozilla Intermediate snippet at /etc/nginx/snippets/ssl-params.conf sets these headers globally on the HTTPS server:

  • Strict-Transport-Security: max-age=63072000; includeSubDomains — pin HSTS for 2 years
  • X-Content-Type-Options: nosniff — block MIME-type sniffing
  • X-Frame-Options: SAMEORIGIN — block click-jacking via cross-origin frames

Verify:

curl -skI https://<domain>/ | grep -iE 'strict-transport|x-content|x-frame'

To override any of them per-vhost, add an add_header directive inside that vhost's server block — NGINX uses the most-specific scope.

Step 9: Server Components

Component Detail
Operating System Ubuntu 24.04 LTS (Noble Numbat)
Web Server NGINX 1.30 (mainline, official nginx.org APT repository)
Cert Tool Certbot 2.9 + python3-certbot-nginx 2.9 plugin
Config Layout /etc/nginx/nginx.conf + /etc/nginx/conf.d/default.conf (single-file vhost)
Document Root /var/www/html
HTTP Port 80 (also used by Certbot HTTP-01 challenge — must stay open from internet for cert issuance and renewal)
HTTPS Port 443 (HTTP/2 enabled)
Fallback Cert /etc/ssl/cloudimg/fallback.crt + /etc/ssl/cloudimg/fallback.key (CN nginx-cloudimg-fallback, RSA 2048, valid 10 years)
SSL Profile Mozilla Intermediate (TLS 1.2 + 1.3) at /etc/nginx/snippets/ssl-params.conf
Renewal Hooks /etc/letsencrypt/renewal-hooks/{pre,post,deploy}/
Default User azureuser (sudo enabled)
Service Management systemd (nginx.service, certbot.timer, certbot.service)
Recommended Size Standard_B2s
VM Generation Hyper-V Gen2 with UEFI boot

Step 10: Managing the NGINX Service

# Test config syntax before reload
sudo nginx -t

# Reload after a config change (zero-downtime)
sudo systemctl reload nginx

# Restart (drops in-flight connections)
sudo systemctl restart nginx

# View access + error logs
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

Step 11: Troubleshooting

Certbot fails with "Some challenges have failed." The most common cause is port 80 not being reachable from the public internet. Check:

# From your workstation, NOT the VM
curl -v http://<domain>/.well-known/acme-challenge/test

A connection-refused or timeout means the NSG is blocking inbound 80. Add the rule from Step 2.

nginx -t reports BIO_new_file ... fopen("...") failed Certbot wrote a path to a cert file that doesn't exist (e.g. you removed /etc/letsencrypt/live/<domain>/). Fix the ssl_certificate and ssl_certificate_key lines in /etc/nginx/conf.d/default.conf back to /etc/ssl/cloudimg/fallback.{crt,key} until the cert is reissued.

Browser still shows the cloudimg fallback cert after certbot --nginx NGINX may not have been reloaded. Run sudo systemctl reload nginx and hard-refresh.

Step 12: Security Recommendations

  • Lock SSH down. Restrict NSG rule allow-ssh to a specific management CIDR; never leave it open to the internet.

  • Rotate the fallback cert if you keep using it. The 10-year self-signed cert is for build-time validation; replace it with Certbot or your own CA cert before serving production traffic.

  • Set up monitoring on certbot.timer. A failed renewal silently leaves an expiring cert in place. Add a simple cron job that runs sudo certbot certificates and alerts if any cert is < 14 days from expiry.

  • Consider a wildcard cert (-d '*.example.com' --preferred-challenges dns) if you serve many subdomains; the DNS-01 challenge requires DNS provider plugins.

  • Tighten file modes on /etc/ssl/cloudimg/fallback.key (must stay 0600) and any deploy hooks under /etc/letsencrypt/renewal-hooks/.

Step 13: Support and Licensing

cloudimg provides 24/7 expert technical support. Contact support@cloudimg.co.uk or visit www.cloudimg.co.uk for the latest documentation and deployment guides.

NGINX is licensed under the 2-clause BSD licence. Certbot is licensed under the Apache 2.0 licence. Ubuntu 24.04 LTS is provided under the standard Canonical terms.

Deploy on Azure

Deploy this image directly from the Azure Marketplace listing.

Need Help?

For questions, support, or custom build requests, contact support@cloudimg.co.uk.