Setting Up Nginx Wildcard SSL With Certbot

Setting Up Nginx Wildcard SSL With Certbot

What You’ll Need


Table of Contents


Why Wildcard SSL Certificates Matter for Modern Stacks

When running microservices, dynamic tenant applications, or webhooks on self-hosted servers, managing individual TLS/SSL certificates for every single subdomain becomes an operational nightmare. If you launch api.yourdomain.com, app.yourdomain.com, and hooks.yourdomain.com, requesting standard Let’s Encrypt HTTP-01 certificates for each subdomain means managing separate renewal timers, handling rate limits, and modifying Nginx configurations every time a service spins up.

A wildcard certificate (*.yourdomain.com) solves this by covering your apex domain and an unlimited number of first-level subdomains with a single cryptographic key pair.

However, obtaining a wildcard SSL certificate from Let’s Encrypt requires passing the DNS-01 challenge. Unlike standard single-domain certificates that rely on the HTTP-01 challenge (where Let’s Encrypt fetches a file placed in your web server’s .well-known/acme-challenge/ directory), wildcard certificates cannot be verified via HTTP. The Certificate Authority (CA) cannot query an arbitrary HTTP path for a subdomain that may not even exist yet. Instead, you must prove control over the underlying DNS zone by publishing a specific TXT record.

If you are setting up infrastructure for background processing or automated webhooks, choosing the right server layout is essential. You can read my comparative guide on How to Set Up a VPS for Automation (Hetzner vs Contabo vs Railway) to determine how to resource your host before proceeding.


Step 1: Provisioning Infrastructure and DNS Records

To issue a wildcard certificate, your domain’s authoritative DNS servers must allow automated API updates, or you must manually add TXT records during the challenge. For production setups, automated DNS-01 validation via API is mandatory.

First, log into your cloud server. I typically deploy Ubuntu 24.04 LTS on a Hetzner VPS or a DigitalOcean droplet.

Ensure your domain name (purchased via Namecheap or transferred to a supported DNS provider like Cloudflare or DigitalOcean) has wildcards pointed to your host server’s static IP address.

1. Configure DNS A/AAAA Records

Navigate to your DNS provider control panel and create two primary A records:

Type: A     Name: @     Value: YOUR_SERVER_IP    TTL: Auto / 300s
Type: A     Name: *     Value: YOUR_SERVER_IP    TTL: Auto / 300s

If your server uses IPv6, add matching AAAA records:

Type: AAAA  Name: @     Value: YOUR_SERVER_IPV6  TTL: Auto / 300s
Type: AAAA  Name: *     Value: YOUR_SERVER_IPV6  TTL: Auto / 300s

2. Verify DNS Propagation

Before invoking Certbot, check that your local resolver recognizes both your base domain and wildcard subdomains:

dig +short example.com
dig +short test.example.com

Both commands must return your server’s public IP address.

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.


Step 2: Installing Certbot and the DNS Authenticator Plugin

Standard Certbot packages provided by Linux distribution repositories can often be outdated. The recommended approach is installing Certbot via snapd or inside a clean Python virtual environment. Here, we will use snap to guarantee access to the latest Let’s Encrypt ACME v2 features and official DNS plugins.

1. Install Certbot via Snap

Run the following commands on your Ubuntu server to remove old versions and install the core Certbot package:

sudo apt-get remove certbot -y
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

2. Install the Required DNS Plugin

Certbot relies on plugins to speak directly to DNS provider APIs during the DNS-01 challenge. Depending on where your domain’s DNS zone is hosted, install the appropriate plugin snap:

For Cloudflare DNS:

sudo snap install certbot-dns-cloudflare
sudo snap set certbot trust-plugin-with-root=ok
sudo snap connect certbot:plugin-dns-cloudflare certbot-dns-cloudflare

For DigitalOcean DNS:

sudo snap install certbot-dns-digitalocean
sudo snap set certbot trust-plugin-with-root=ok
sudo snap connect certbot:plugin-dns-digitalocean certbot-dns-digitalocean

3. Generate API Credentials File

Create a secure directory to store your DNS provider API tokens. Never expose this secret file to unprivileged system users.

sudo mkdir -p /etc/letsencrypt/credentials
sudo touch /etc/letsencrypt/credentials/dns_api.ini
sudo chmod 600 /etc/letsencrypt/credentials/dns_api.ini

Edit /etc/letsencrypt/credentials/dns_api.ini using nano or vim. Add your provider credentials:

For Cloudflare API Token (dns_cloudflare_api_token):

dns_cloudflare_api_token = 1234567890abcdef1234567890abcdef12345678

For DigitalOcean API Token (dns_digitalocean_token):

dns_digitalocean_token = dop_v1_1234567890abcdef1234567890abcdef1234567890abcdef

4. Issue the Wildcard Certificate

Execute Certbot specifying both the root domain (example.com) and the wildcard identifier (*.example.com). Including both ensures that requests to https://example.com and https://sub.example.com are served by the exact same SSL certificate.

Using Cloudflare plugin:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/credentials/dns_api.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d example.com \
  -d "*.example.com" \
  --agree-tos \
  -m admin@example.com \
  --no-eff-email

If using DigitalOcean DNS, the syntax mirrors Cloudflare:

sudo certbot certonly \
  --dns-digitalocean \
  --dns-digitalocean-credentials /etc/letsencrypt/credentials/dns_api.ini \
  --dns-digitalocean-propagation-seconds 30 \
  -d example.com \
  -d "*.example.com" \
  --agree-tos \
  -m admin@example.com \
  --no-eff-email

Upon success, Certbot outputs the paths to your full certificate chain and private key:

  • Certificate chain: /etc/letsencrypt/live/example.com/fullchain.pem
  • Private key: /etc/letsencrypt/live/example.com/privkey.pem

Step 3: Configuring Nginx to Use the Wildcard Certificate

With the wildcard certificate generated, we now configure Nginx to handle incoming HTTPS connections. We will build a complete, production-grade configuration including TLS modern standards, HTTP-to-HTTPS redirection, Diffie-Hellman parameters, and dynamic host routing.

1. Generate Strong Diffie-Hellman Parameters

Generate a 2048-bit Diffie-Hellman group to strengthen key exchange protocols:

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

2. Create the Primary Nginx Site Configuration

Remove the default site config and create /etc/nginx/sites-available/wildcard.conf:

sudo rm -f /etc/nginx/sites-enabled/default
sudo nano /etc/nginx/sites-available/wildcard.conf

Write the following full configuration without placeholders. Replace example.com with your actual domain name:

# Upstream definition for generic internal backend service
upstream default_app_backend {
    server 127.0.0.1:8080;
}

# Upstream definition for an automation platform endpoint
upstream n8n_workflow_backend {
    server 127.0.0.1:5678;
}

# Port 80 HTTP Server Block - Redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com *.example.com;

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

# Port 443 HTTPS Catch-all Wildcard Server Block
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com *.example.com;

    # SSL Certificate Paths
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    # Session configuration
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    # Cryptographic protocols & ciphers
    ssl_protocols TLSv1.2 TLSv1.3;
    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;
    ssl_prefer_server_ciphers off;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Default proxying logic
    location / {
        proxy_pass http://default_app_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;
    }
}

# Dedicated Server Block for Specific Subdomains (e.g., hooks.example.com)
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name hooks.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    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;

    location / {
        proxy_pass http://n8n_workflow_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

If you are setting up isolated endpoints to trigger automation sequences, review my guide on Building Custom Webhooks vs Using n8n Triggers to structure your ingress routes securely behind Nginx. Furthermore, if your backend proxies outbound calls to public services, consult The Best Free APIs for Building Automation Workflows in 2026 to leverage reliable public endpoints.

Enable the configuration file by creating a symlink into sites-enabled, test the syntax, and reload Nginx:

sudo ln -s /etc/nginx/sites-available/wildcard.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 4: Automating Wildcard Certificate Renewals

Let’s Encrypt certificates expire after 90 days. Certbot installs a background Systemd timer (certbot.timer) automatically. However, since wildcard renewals occur non-interactively via DNS-01 API credentials, Nginx must be instructed to reload its configuration to pick up the updated keys without dropping active TLS connections.

1. Test Renewal via Dry Run

Run a full renewal simulation to verify that the DNS plugin can authenticate with the API and update TXT records without user interaction:

sudo certbot renew --dry-run

If the dry run succeeds, your setup is non-interactive and ready for production automation.

2. Configure Post-Renewal Hooks

Instead of restarting Nginx continuously, we can leverage Certbot hooks to issue a light reload (systemctl reload nginx) only when a certificate is successfully renewed.

Create a deployment script inside /etc/letsencrypt/renewal-hooks/deploy/:

sudo touch /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Add the following content to /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh:

#!/usr

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online