Wildcard SSL certificates with Let's Encrypt (DNS-01), and fixing mixed content
Issue a *.example.com certificate with certbot and DNS validation, keep it renewing, and clear the mixed-content warnings that follow.

A wildcard certificate covers every subdomain of a domain, *.example.com, including ones you create later. Let's Encrypt issues them free, but only through DNS validation, which is a little different from the usual setup. This guide uses certbot on an Ubuntu Cloud Server running nginx, then covers the mixed-content warnings that often show up once a site moves to HTTPS.
Why wildcards need DNS validation
An ordinary certificate is validated over HTTP: Let's Encrypt fetches a file from your server to prove you control the domain. A wildcard has no single hostname to fetch from, so validation uses the DNS-01 challenge instead. You prove control by publishing a TXT record at _acme-challenge.example.com.
That means two things:
- The server does not need to be reachable for validation, but you do need access to wherever your DNS is hosted.
- If you want renewal to be automatic, certbot needs a way to update DNS by itself, through a DNS provider plugin.
A wildcard covers one level only. *.example.com covers shop.example.com but not example.com itself or a.b.example.com, so always request the bare domain as well.
Option A: automatic renewal with a DNS plugin (recommended)
If your DNS is hosted with a provider that has a certbot plugin, such as Cloudflare, Route 53 or DigitalOcean, use it. This example uses Cloudflare.
apt updateapt install -y certbot python3-certbot-dns-cloudflare
Create an API token at your DNS provider limited to editing DNS for this one zone, and store it where only root can read it:
mkdir -p /root/.secretscat > /root/.secrets/cloudflare.ini <<'INI'dns_cloudflare_api_token = your-scoped-api-tokenINIchmod 600 /root/.secrets/cloudflare.ini
Request the certificate:
certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \ -d example.com -d '*.example.com'
Renewal is handled by the systemd timer the package installs. Confirm it works end to end:
certbot renew --dry-run
Option B: manual DNS validation
If your DNS provider has no plugin, certbot can ask you to add the record by hand:
certbot certonly --manual --preferred-challenges dns \ -d example.com -d '*.example.com'
Certbot shows a TXT value for _acme-challenge.example.com. Add it at your DNS provider, wait for it to be visible, then press Enter. Check it from another terminal first:
dig +short TXT _acme-challenge.example.com @1.1.1.1
Tip: a manually validated certificate does not renew on its own. Let's Encrypt certificates last 90 days, so either repeat this before expiry or move to a DNS plugin. Put a reminder in your calendar for day 60 the moment you issue it.
Use the certificate in nginx
Certificates are written to /etc/letsencrypt/live/example.com/:
server { listen 443 ssl; 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; # ...} server { listen 80; server_name example.com *.example.com; return 301 https://$host$request_uri;}
nginx -t && systemctl reload nginx
So nginx picks up renewed certificates, add a deploy hook:
echo 'systemctl reload nginx' > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shchmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Check it worked
echo | openssl s_client -connect anything.example.com:443 -servername anything.example.com 2>/dev/null \ | openssl x509 -noout -subject -ext subjectAltName -dates
The names should include *.example.com and the dates should be current. You need a DNS record (a wildcard * A record works) pointing the subdomain at 203.0.113.10 for this test.
Fixing mixed-content warnings
Your certificate is valid but the browser still shows a broken padlock, or the page loads without styling. That is mixed content: an HTTPS page loading images, stylesheets or scripts over plain HTTP. Browsers block scripts and stylesheets outright, which is why a site can suddenly look unstyled, and warn about images.
Find the offending resources
Open the page, then the browser's developer tools (right-click, Inspect, then the Console tab) and reload. Each warning names a URL beginning http://. That is your list.
You can also search your code and templates:
grep -rn "http://" /var/www/example.com --include='*.html' --include='*.php' --include='*.css' --include='*.js'
Fix the references
<!-- Wrong --><img src="http://example.com/logo.png"> <!-- Right --><img src="/logo.png"><img src="https://example.com/logo.png">
Relative paths are best, since they work regardless of protocol or domain. For a resource on a third-party site, switch to its HTTPS URL; if it has none, host the file yourself.
If the site is a CMS, also update its configured site URL to https://. For WordPress, update both WordPress Address and Site Address, then replace old URLs in the database with WP-CLI, which handles serialized data correctly:
wp search-replace 'http://example.com' 'https://example.com' --all-tables
Add a safety net
This header tells browsers to upgrade any remaining HTTP requests on your pages automatically:
add_header Content-Security-Policy "upgrade-insecure-requests" always;
Test in a private window afterwards. A cached copy of the page can keep showing the old warnings.
Next steps
- Serve static assets from Object Storage over HTTPS.
- Protect the site at the edge with included DDoS protection.
- Deploy a Cloud Server close to your visitors.
Try it yourself
Cloud Servers start at $3.25/mo, billed by the hour.