Thumbnail

How to Set Up a Wildcard SSL Certificate with Lets Encrypt and Nginx

A wildcard certificate covers *.yourdomain.com with a single cert instead of one per subdomain. This matters operationally when you are adding subdomains frequently: without a wildcard, every new subdomain requires a separate certificate issuance, a separate renewal job, and a separate Nginx configuration block pointing at it.

Let's Encrypt issues wildcard certificates for free. The trade-off is that wildcards can only be validated via the DNS-01 challenge — you cannot use the simpler HTTP-01 method. Let's Encrypt certificates are valid for 90 days, which means automation is not optional. A certificate that expires because a manual renewal was missed takes a service offline with no gradual degradation warning.

The DNS-01 Challenge

The DNS-01 challenge proves domain ownership by having you create a _acme-challenge TXT record at your DNS provider. Let's Encrypt queries for that record, validates it, and issues the certificate. For *.devopsbyexample.io, the required record looks like this:

_acme-challenge.devopsbyexample.io  TXT  <random-token-from-letsencrypt>

This must be repeated at every renewal (every ~60 days). With --manual mode, that means logging into your DNS provider and updating the record by hand each time. For anything beyond a one-off test, automate this with a Certbot DNS plugin that talks directly to your DNS provider's API.

Prerequisites

This guide assumes you already have Nginx and Certbot installed. You can verify Certbot is available by running:

certbot --version

Step 1: Obtain the Wildcard Certificate

Always test with the Let's Encrypt staging environment first. It has no quota limits and lets you verify everything works before going to production.

certbot certonly \
  --manual \
  --preferred-challenges dns \
  --test-cert \
  -d "*.devopsbyexample.io"

Once staging succeeds, remove --test-cert and run for real:

certbot certonly \
  --manual \
  --preferred-challenges dns \
  -d "*.devopsbyexample.io"

Follow the prompts:

  1. Enter your email address for emergency notifications.
  2. Agree to the Terms of Service.
  3. Enter your domain with the wildcard prefix: *.devopsbyexample.io

Certbot will output a TXT record value that you need to add to your DNS.

Step 2: Create the DNS TXT Record

Log in to your DNS provider (e.g., Google Domains, Cloudflare, Route 53) and create the following record:

Field Value
Type TXT
Host / Name _acme-challenge
Value <token provided by Certbot>
TTL 300 seconds

After saving, wait a few minutes for propagation. You can verify with dig:

dig -t TXT _acme-challenge.devopsbyexample.io +short

Note: DNS providers often use anycast, meaning Let's Encrypt may query a different server than your local dig command. Propagation can take anywhere from a few minutes to an hour. Some providers offer an API to check full propagation.

Once you're confident the record is live, press Enter in the Certbot prompt. If successful, the certificate and private key will be saved under:

  • Certificate: /etc/letsencrypt/live/devopsbyexample.io/fullchain.pem
  • Private Key: /etc/letsencrypt/live/devopsbyexample.io/privkey.pem

Keep note of both paths, you'll need them for Nginx.

Verify the Certificate (Optional)

You can inspect the certificate with OpenSSL to confirm it's a wildcard:

openssl x509 -in /etc/letsencrypt/live/devopsbyexample.io/fullchain.pem -text -noout

Look for the Subject Alternative Name field, which should show *.devopsbyexample.io.

Step 3: Set Up a Simple HTML Page

Create a directory for your web files and set the correct permissions:

mkdir -p /var/www/mysite/html
chmod 755 /var/www/mysite/html

Add a simple index.html:

<html>
  <head><title>My Site</title></head>
  <body><h1>Hello from Nginx!</h1></body>
</html>

Step 4: Configure Nginx

Create a new config file at /etc/nginx/conf.d/mysite.conf.

HTTPS Server Block

server {
    listen 443 ssl;
    server_name *.devopsbyexample.io;

    ssl_certificate     /etc/letsencrypt/live/devopsbyexample.io/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/devopsbyexample.io/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

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

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    root /var/www/mysite/html;
    index index.html;
}

HTTP to HTTPS Redirect

server {
    listen 80;
    server_name *.devopsbyexample.io;
    return 301 https://$host$request_uri;
}

Common gotcha: Make sure your server_name uses *.devopsbyexample.io (with a dot before the domain), not *devopsbyexample.io.

Test and reload Nginx:

nginx -t
nginx -s reload

Step 5: Add DNS A Records for Your Subdomains

For each subdomain you want to serve, add an A record pointing to your server's public IP:

Subdomain Type Value
api A <your-server-ip>
hello A <your-server-ip>

Verify with dig:

dig api.devopsbyexample.io
dig hello.devopsbyexample.io

Step 6: Test in the Browser

Open https://api.devopsbyexample.io and https://hello.devopsbyexample.io in a private/incognito window. You should see the green lock and your HTML page for any subdomain.

Automating Renewal

With --manual, renewal requires running the same interactive process every 60 days. That is not sustainable. The right approach is a Certbot DNS plugin that talks to your DNS provider's API and handles the _acme-challenge TXT record automatically.

For Cloudflare:

pip install certbot-dns-cloudflare

Create a credentials file:

# /etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = <your-cloudflare-api-token>
chmod 600 /etc/letsencrypt/cloudflare.ini

Issue the certificate with DNS plugin instead of --manual:

certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.devopsbyexample.io"

With this in place, certbot renew can run unattended via a systemd timer or cron job and will update the DNS record automatically:

# /etc/cron.d/certbot
0 */12 * * * root certbot renew --quiet

Plugins exist for Route 53 (certbot-dns-route53), Google Cloud DNS (certbot-dns-google), and most major providers. For Route 53, the plugin uses boto3 credentials, so IAM role-based access works if you are running on EC2.

A common failure mode during automated renewal: the DNS plugin credentials file has been rotated or the IAM role permissions have changed since initial issuance. Test renewals in advance with certbot renew --dry-run and monitor for failures — a silent renewal failure that is not caught early results in an expired certificate with no warning.

Summary

Step What It Does
DNS-01 challenge Proves domain ownership via a TXT record at _acme-challenge.<domain>
Certbot with DNS plugin Automates TXT record creation and renewal without manual DNS edits
Nginx HTTPS block TLS 1.2/1.3 only, strong cipher suite, HSTS header, session cache
HTTP redirect Forces all traffic to HTTPS with a permanent 301
Dry-run renewal test Validates the full renewal path before the certificate actually expires

The critical thing to get right is renewal automation. A wildcard certificate that expires silently takes down every subdomain at once. Test the renewal path with certbot renew --dry-run after initial setup and confirm the cron or systemd timer is actually running before considering this done.

Comments