Skip to content
Tutorials

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.

Marcus ReedAug 17, 2026 · 5 min read

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.

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

Marcus ReedMarcus helps developers ship on Infraista, turning support questions into guides from his base in Dallas.

Try it yourself

Cloud Servers start at $3.25/mo, billed by the hour.