
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 --versionStep 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:
- Enter your email address for emergency notifications.
- Agree to the Terms of Service.
- 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 +shortNote: DNS providers often use anycast, meaning Let's Encrypt may query a different server than your local
digcommand. 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 -nooutLook 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/htmlAdd 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_nameuses*.devopsbyexample.io(with a dot before the domain), not*devopsbyexample.io.
Test and reload Nginx:
nginx -t
nginx -s reloadStep 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.ioStep 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-cloudflareCreate a credentials file:
# /etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = <your-cloudflare-api-token>chmod 600 /etc/letsencrypt/cloudflare.iniIssue 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 --quietPlugins 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-runand 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