Configuring Nginx SSL Certificates for Custom Subdomains

Configuring Nginx SSL Certificates for Custom Subdomains

What You’ll Need

Before diving into this guide, ensure you have access to the following resources:

Table of Contents


Understanding Subdomain Architecture and Wildcard Certificates

Deploying microservices, multi-tenant web applications, or workflow automation platforms often requires dedicated subdomains. Managing individual TLS/SSL certificates for api.example.com, app.example.com, and webhook.example.com becomes tedious quickly. Every new subdomain forces you to provision a new HTTP-01 challenge, open firewall ports, and update web server configurations.

Wildcard certificates solve this operational overhead. A single certificate issued for *.example.com secures any first-level subdomain under your domain name. Combined with Nginx as a reverse proxy, you can rapidly spin up backend microservices without issuing a new certificate every single time.

To issue a wildcard certificate through Let’s Encrypt, you cannot use standard HTTP-01 verification because Let’s Encrypt needs proof that you control the entire domain zone. Instead, you must use the DNS-01 challenge. This validation method requires inserting a specific TXT record into your domain’s DNS zone file.

When you scale this setup, manually creating TXT records isn’t viable. We automate this process using Certbot DNS plugins, which interact directly with your provider’s API to handle creation, verification, and cleanup automatically during renewals.


Setting Up DNS and Nginx Core Configurations

To begin, point your domain and its wildcard subdomains to your server. Log into your account on Namecheap or your chosen DNS host and create two A records:

  1. @ pointing to your server’s public IP address.
  2. * pointing to your server’s public IP address.

This setup routes all requests for non-explicitly defined subdomains directly to your Nginx proxy running on your Hetzner VPS.

Next, connect to your server via SSH and install Nginx alongside the necessary system utilities.

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx curl ufw

Enable HTTP and HTTPS traffic through the Uncomplicated Firewall (UFW):

sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable

Before obtaining the wildcard certificate, establish a foundational Nginx server configuration. This initial layout sets up a base HTTP server block designed to force all incoming connections to redirect to HTTPS once certificates are installed. If you are unfamiliar with fundamental reverse proxy architecture, read our guide on Configuring Nginx Reverse Proxy with Certbot SSL to understand standard single-domain configurations.

Create a baseline proxy site file at /etc/nginx/sites-available/default-wildcard.conf:

server {
    listen 80;
    listen [::]:80;
    server_name example.com *.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

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

Link this file to the active directory and remove the stock default file:

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

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


Generating Wildcard SSL Certificates with Certbot DNS Plugin

Because HTTP-01 validation cannot issue *.example.com certificates, we use the DNS-01 challenge. Certbot offers plugins for major DNS providers including Cloudflare, DigitalOcean, Route53, and Namecheap.

In this example, we will install and configure the Cloudflare DNS plugin, which is widely used for automated DNS management. If you host DNS directly on DigitalOcean, you can install python3-certbot-dns-digitalocean instead.

Install the plugin:

sudo apt install -y python3-certbot-dns-cloudflare

Create a secure directory for API credentials:

sudo mkdir -p /etc/letsencrypt/secrets
sudo chmod 700 /etc/letsencrypt/secrets

Create the credentials file at /etc/letsencrypt/secrets/cloudflare.ini:

dns_cloudflare_api_token = 1234567890abcdef1234567890abcdef12345678

Set strict read/write permissions on the file to prevent unauthorized access to your API key:

sudo chmod 600 /etc/letsencrypt/secrets/cloudflare.ini

Run Certbot to request the wildcard certificate. Replace example.com with your real domain:

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

Certbot creates key files in /etc/letsencrypt/live/example.com/:

  • fullchain.pem: Your certificate bundled with intermediate certificates.
  • privkey.pem: Your private key.

Verify that automatic renewal works without manual intervention:

sudo certbot renew --dry-run

Because wildcards secure sensitive endpoint structures like webhook consumers and API endpoints, security must extend beyond TLS. Ensure your applications validate payload integrity when exposed publicly. For details on securing publicly reachable endpoints, see our walkthrough on Securing Incoming Webhooks with HMAC Signature Verification.


Dynamic Subdomain Routing and Automated SSL for Multi-Tenant Services

Once your wildcard certificate is in place, you can route traffic dynamically to multiple backend services based on the requested host header. This eliminates the need to edit Nginx files whenever a new subdomain is added.

Here is a full, production-ready Nginx configuration that handles dynamic upstream routing, forces TLS 1.2 and 1.3, applies secure headers, and maps specific subdomains to internal microservice ports.

Create the file /etc/nginx/sites-available/wildcard-proxy.conf:

map $host $backend_port {
    default 8080;
    app.example.com 3000;
    api.example.com 4000;
    n8n.example.com 5678;
    webhooks.example.com 9000;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com *.example.com;

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

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com *.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.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;
    ssl_prefer_server_ciphers off;

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

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    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 "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    access_log /var/log/nginx/wildcard_access.log;
    error_log /var/log/nginx/wildcard_error.log warn;

    location / {
        proxy_pass http://127.0.0.1:$backend_port;
        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;

        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        proxy_buffering off;
    }
}

Enable the configuration and reload Nginx:

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

With this pattern, adding a new service to your infrastructure requires only two steps:

  1. Spin up your service on an internal port (for example, port 5000 for a new service).
  2. Add service.example.com 5000; inside the Nginx map block and reload Nginx.

No new SSL certificates are required, and zero domain challenge delays are introduced.

This backend proxy configuration integrates directly with automation environments. For instance, if you run automation software like n8n Cloud or self-host n8n on n8n.example.com, you can seamlessly route webhook triggers to business tools. Learn how to leverage automated webhook triggers by checking out our guide to Automate WhatsApp Business Messages with n8n and 360dialog.

To verify your SSL setup, test your SSL deployment using curl:

curl -I -v https://app.example.com

The output should show HTTP/2 support, an active TLS 1.3 connection, valid certificate authority details from Let’s Encrypt, and proper security response headers.


Getting Started

To implement this architecture on your server, assemble your hosting environment and configure your records:

  1. Provision a high-performance VPS on Hetzner VPS or Contabo VPS.
  2. Domain owners can quickly link DNS zones via Namecheap or manage infrastructure components directly using DigitalOcean.
  3. Connect your applications or self-hosted services using n8n Cloud for workflow automation across subdomains.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

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