Let's Encrypt wildcard certificate over DNS validation

Published on 15 min read

A wildcard certificate cannot be validated through the web server, it always goes through a TXT record in DNS. This guide shows the manual and the automatic route on Debian 13, Debian 12, Ubuntu 24.04 and Ubuntu 22.04, plus the errors it actually fails on.

A wildcard certificate covers every name on one level: shop.MeineDomain.de, mail.MeineDomain.de, kunde-4711.MeineDomain.de, including names that do not exist yet. That is exactly why the familiar route through the web server no longer works here. This guide shows both workable approaches, the manual one and the automatic one, and pays particular attention to the places where things go wrong in practice.

Everything below has been verified on Debian 13, Debian 12, Ubuntu 24.04 LTS and Ubuntu 22.04 LTS. The four systems differ more in their Certbot packaging than most guides admit, so there is a table further down.

Why a wildcard certificate only works over DNS

Let's Encrypt knows three validation methods. Two of them are ruled out for wildcards:

  • HTTP-01 places a file at http://name/.well-known/acme-challenge/token. For *.MeineDomain.de there is no single name under which that file could sit. The certificate authority would have to query an infinite number of hostnames.
  • TLS-ALPN-01 has the same problem, it also validates one specific host on port 443.
  • DNS-01 checks a TXT record at _acme-challenge.MeineDomain.de. Whoever can set that record controls the zone and therefore every name below it. That is the only proof that fits a star.

In practice this means that --apache, --nginx, --webroot and --standalone cannot be used for wildcards. Try it anyway and you get this message:

Client with the currently selected authenticator does not support any
combination of challenges that will satisfy the CA. You may need to use an
authenticator plugin that can do challenges over DNS.

That is not a mistake in your configuration, it is the correct answer to an impossible request. For the normal case with a handful of fixed names the web server route is still the right one, and we describe it in the article on the free SSL certificate with Certbot.

A second point that almost every guide leaves out: a wildcard certificate covers only the names one level down. *.MeineDomain.de applies to shop.MeineDomain.de, but neither to MeineDomain.de itself nor to a.b.MeineDomain.de. You have to request the bare domain in addition, and that has consequences for the TXT record, see below.

Requirements and the package situation per distribution

You need root access over SSH, a domain whose zone you manage, and Certbot. A running web server is not required for issuance, and port 80 does not have to be open. That is a pleasant side effect: you can issue a certificate for a service that is not connected to the internet at all.

Install Certbot and the DNS tools:

apt update
apt install -y certbot bind9-dnsutils

The bind9-dnsutils package provides dig. On all four systems the old name dnsutils is nothing more than a placeholder that points to bind9-dnsutils. Check which Certbot version you ended up with:

certbot --version

The differences are considerable, and they decide which route is open to you at all:

SystemCertbotPlugins from the distribution repositories
Debian 134.0.0cloudflare, desec, google, infomaniak, rfc2136, route53
Debian 122.1.0cloudflare, digitalocean, dnsimple, gandi, gehirn, google, linode, ovh, rfc2136, route53, sakuracloud
Ubuntu 24.042.9.0cloudflare, digitalocean, dnsimple, gandi, gehirn, google, infomaniak, linode, ovh, rfc2136, route53, sakuracloud
Ubuntu 22.041.21.0cloudflare, digitalocean, dnsimple, gandi, gehirn, google, linode, ovh, rfc2136, route53, sakuracloud

And here is the surprise: Debian 13 has thrown most of the DNS plugins out of the archive. If you worked with python3-certbot-dns-ovh or python3-certbot-dns-linode on Debian 12 and then upgrade to Debian 13, the package is simply gone. An apt upgrade across the distribution boundary removes it, and renewal breaks without anyone looking. Check this before you change distribution.

To see which plugins are actually loaded on your system:

certbot plugins

The manual route with a TXT record

The manual route needs no API access and works with every DNS provider. It is fine for trying things out and for zones you rarely touch anyway. It has one serious drawback, which we come to in a moment.

Lower the TTL of the future TXT record in your zone beforehand, 60 to 300 seconds. That costs nothing and saves you waiting time later. Then:

certbot certonly --manual --preferred-challenges dns \
  --cert-name meinedomain.de \
  -d "*.MeineDomain.de" -d MeineDomain.de

The quotes around "*.MeineDomain.de" are mandatory. Without them the shell replaces the star with filenames from the current directory, and Certbot requests certificates for your files. --cert-name is strongly recommended as well: otherwise Certbot derives the name of the certificate directory from the first name, and nobody wants to go hunting for a directory with a star in its name.

Certbot pauses and shows something like this:

Please deploy a DNS TXT record under the name:
_acme-challenge.MeineDomain.de.

with the following value:

gfj9Xq...Rg85nM

Now comes the point where most attempts fail. You requested two names, the star and the bare domain. Those are two separate validations, and both land on the same record name _acme-challenge.MeineDomain.de, with two different values. Certbot says so too:

This must be set up in addition to the previous challenges; do not remove, replace, or undo the previous challenge tasks yet. Note that you might be asked to create multiple distinct TXT records with the same name. This is permitted by DNS standards.

Many DNS interfaces, however, offer only one input field for the same name and replace the first value with the second. Only one value then ends up in the zone, one of the two validations fails, and the error message still mentions a single name. If your interface does not allow two TXT records with the same name, that rules out the manual route. Use CNAME delegation instead, see further down.

Check first, then press Enter

Certbot waits for your confirmation. Do not press Enter straight away. Open a second SSH session and query the authoritative nameserver first, not your local resolver:

dig +short NS MeineDomain.de
dig +short TXT _acme-challenge.MeineDomain.de @ns1.anbieter.example
dig +short TXT _acme-challenge.MeineDomain.de @1.1.1.1
dig +short TXT _acme-challenge.MeineDomain.de @8.8.8.8

Only once both values show up in one query, and from several independent resolvers, do you press Enter. Empty output means the record is not there yet. This is what it looks like for a domain without the record, the output stays empty:

dig +short TXT _acme-challenge.example.com @1.1.1.1

Why the detour via the authoritative server? If you query _acme-challenge before you have created the record, your resolver remembers the non-existence for the length of the negative TTL from the SOA record, often a full hour. You then see nothing for a long time although the record has been in place for a while, and you look for the fault in the wrong place. The authoritative server has no such cache.

The catch with the manual route

A certificate issued with --manual and without a script never renews itself. The next automatic run logs this:

An authentication script must be provided with --manual-auth-hook when using
the manual plugin non-interactively.

Certbot skips that certificate and carries on with the others, and the exit code often looks harmless. You notice when the browser complains. So plan firmly on repeating the same procedure by hand every 60 to 90 days, or switch to one of the automatic routes.

The automatic route with a provider plugin

If your DNS provider has an API and there is a Certbot plugin for it, Certbot sets the TXT record itself, waits, has it validated and cleans it up again. This is the route you want for production systems. Using Cloudflare as the example:

apt install -y python3-certbot-dns-cloudflare

Store the credentials outside the web directory and protect them immediately:

mkdir -p /root/.secrets/certbot
chmod 700 /root/.secrets/certbot

Exactly one line belongs in /root/.secrets/certbot/cloudflare.ini:

dns_cloudflare_api_token = IhrTokenHier

After that, without fail:

chmod 600 /root/.secrets/certbot/cloudflare.ini

Otherwise Certbot warns about overly broad permissions on every run. Use a restricted token with write permission on DNS records, not the global account key. The global key does still work, but it can do anything in your account, and afterwards it sits in plain text on the server. A token can be limited to a single zone and revoked on its own if something goes wrong.

Issuing the certificate:

certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 60 \
  --cert-name meinedomain.de \
  -d "*.MeineDomain.de" -d MeineDomain.de

The value for --propagation-seconds is the waiting time between setting the record and the validation request. The default is 10 seconds and is too tight for many zones. 60 seconds is a good starting point, 120 with slow providers. This single number explains a large share of the sporadically failing renewals that nobody else can reproduce.

For other providers the package is called python3-certbot-dns-<anbieter>, and the parameters follow the same pattern. Check the table above to see whether your provider is included on your distribution at all.

When the matching package is missing

The obvious reflex is pip install certbot-dns-irgendwas. On Debian 12, Debian 13 and Ubuntu 24.04 that ends like this:

error: externally-managed-environment

× This environment is externally managed

That is deliberate, not a defect. On Ubuntu 22.04 the same command still goes through, but it then mixes pip packages in among the system packages, and at the next apt upgrade the versions of Certbot and plugin no longer match. Do not force it with --break-system-packages.

The clean way out is the snap build of Certbot, which brings the plugins along and keeps itself up to date. Remove the distribution package first so that two Certbots do not manage the same directory:

apt remove -y certbot
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot
snap set certbot trust-plugin-with-root=ok
snap install certbot-dns-cloudflare

Important: the existing data under /etc/letsencrypt/ is preserved, the snap takes it over. But the systemd timer is called snap.certbot.renew.timer afterwards and no longer certbot.timer. Miss that and you end up with two timers or with none.

Without a provider plugin: rfc2136 and CNAME delegation

Two methods work no matter who your DNS provider is.

rfc2136 is the standard route for dynamic DNS updates with a TSIG key. It works with BIND, Knot and PowerDNS and is available as a package on all four systems:

apt install -y python3-certbot-dns-rfc2136

If you run your own DNS, this is the most robust solution, because it needs no third-party service and no HTTP API.

CNAME delegation is the more elegant answer to two problems at once. You create one immutable record in your main zone, once and for all:

_acme-challenge.MeineDomain.de.  CNAME  MeineDomain.de.acme.eine-andere-zone.de.

Let's Encrypt follows CNAME chains when it looks for the TXT record. The TXT record is therefore created in the target zone, and only there does the server need write permission. That settles several points at once:

  • The credentials on the web server cannot modify your main zone. A compromised web server cannot redirect MX records.
  • The target zone may hold several TXT values at the same time, even if the interface of your main provider cannot.
  • The target zone can have a very low TTL without the main zone suffering for it.

The CNAME itself is never touched again, so it may have a high TTL. You can check it with:

dig +short CNAME _acme-challenge.MeineDomain.de @1.1.1.1

Automating the renewal

The package already ships a timer that runs twice a day. A certificate is only renewed when that is actually necessary:

systemctl list-timers certbot.timer

If it is not running:

systemctl enable --now certbot.timer

On all four systems the package ships two triggers: /lib/systemd/system/certbot.timer and additionally /etc/cron.d/certbot. The cron file checks at the start whether the timer is active and then does nothing, so nothing gets renewed twice. You therefore do not need a renewal cron job of your own, it would be the third trigger for the same task.

There is one difference between the distributions worth knowing about. Certbot up to version 3 renews when fewer than 30 days of validity are left. Certbot 4.0, the build in Debian 13, renews when a third of the lifetime remains instead. With the 90 days that are common today both arrive at the same moment. As soon as Let's Encrypt issues shorter-lived certificates, the two builds behave differently, and only the new one adapts by itself.

Test the process as a dry run. Nothing is issued and no rate limit is consumed:

certbot renew --dry-run

Certbot does not write a wildcard certificate into the web server configuration, you have to do that once yourself. To make sure the web server really loads the new certificate after every renewal, create a deploy hook. Write it as a file, not as a parameter:

mkdir -p /etc/letsencrypt/renewal-hooks/deploy

The contents of /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh:

#!/bin/sh
systemctl reload nginx

Then make it executable:

chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh

Scripts in this directory run for every renewed certificate. The --deploy-hook parameter, by contrast, is only written into the renewal file of exactly those certificates that were handled at that moment. Certificates added later do not have it, and that only surfaces months afterwards.

A warning about the API credentials: if the token is revoked at the provider or expires, renewal fails without anything being visibly broken. The certificate is still valid, after all. Only 30 days later does the service go down. So check now and then that certificates really are getting fresher, and do not rely on expiry warnings by email.

How to tell that it really worked

The fact that the command ran without an error is no proof. These three checks are. First the overview:

certbot certificates

Both names have to appear under Domains, *.MeineDomain.de and MeineDomain.de. If the star is missing, you got an ordinary certificate and simply did not notice.

Second, a look into the file itself:

openssl x509 -noout -text -in /etc/letsencrypt/live/meinedomain.de/fullchain.pem | grep -A1 "Subject Alternative Name"

DNS:*.MeineDomain.de has to show up there. The field that counts is Subject Alternative Name, not the Common Name, which modern browsers no longer evaluate at all.

Third, and this is the actual proof, query a name you have just made up:

echo | openssl s_client -servername test-1234.MeineDomain.de -connect MeineDomain.de:443 2>/dev/null | openssl x509 -noout -subject -dates

If that returns a valid certificate and no warning, the wildcard really does apply. Only then are you finished.

Common error messages, word for word

  • "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.MeineDomain.de": the record does not exist, has not propagated yet, or you created it at the wrong provider. The most common case: the domain is registered with provider A, but the nameservers point to provider B, and the record sits at A. The only thing that counts is what dig +short NS MeineDomain.de outputs.
  • "Incorrect TXT record ... found at _acme-challenge.MeineDomain.de": there is a value, but the wrong one. Typical after an abort, when the old value is still in the zone, or when the interface wrote the second value over the first. Delete old _acme-challenge records and start over.
  • "DNS problem: SERVFAIL looking up TXT ... the domain's nameservers may be malfunctioning": almost always a broken DNSSEC signature, for example after a provider change where the old DS record stayed behind at the registry. Repair that first, otherwise every issuance fails.
  • "CAA record for MeineDomain.de prevents issuance": the stumbling block that often gets overlooked. For wildcards the certificate authority evaluates issuewild first. If you set issue "letsencrypt.org" but have an issuewild ";" sitting next to it, you get ordinary certificates and no wildcards. Check with dig +short CAA MeineDomain.de.
  • "too many certificates (5) already issued for this exact set of identifiers": five certificates in seven days are allowed for the same combination of names. So test with --dry-run or against the staging environment using --test-cert. The block expires by itself and cannot be lifted.
  • The interface shows the TXT record, but dig does not: some providers require changes to the zone to be published explicitly. A saved record is not automatically an active record.
  • The record name ends up doubled: some interfaces append the domain automatically. Enter only _acme-challenge there, otherwise you get _acme-challenge.MeineDomain.de.MeineDomain.de. A dig on the full name exposes that immediately.

Outlook: DNS-PERSIST-01

Let's Encrypt is working on a new validation method called DNS-PERSIST-01. Instead of publishing a fresh token at every renewal, you store one permanent record that authorizes a specific ACME account to issue certificates. After that the server needs no write access to DNS for renewal at all. For wildcards that would be a clear gain in security.

According to the Let's Encrypt roadmap the staging environment was scheduled for the end of the first quarter of 2026 and production operation for the second quarter. Whether Certbot will support the method has not been announced so far. So do not plan around it yet, but keep an eye on it if you are currently building a new certificate management setup.

Summary

A wildcard certificate is only possible through DNS validation, because a star cannot be proven through a single web server. The manual route with a TXT record works everywhere, but it never renews itself. The route through a provider plugin is the only one you can leave running unattended, and in practice it usually fails on too short a waiting time or on credentials that have quietly become invalid. If you remember only one thing from this: check the TXT record at the authoritative nameserver before you let Certbot continue, and with a star plus the bare domain expect two records under the same name.

Frequently asked questions

Why can't I issue a wildcard certificate through Apache or nginx?
Because web server validation asks for a file under one specific hostname. A wildcard covers any number of names, however, including names that do not exist yet. For wildcards Let's Encrypt therefore accepts DNS validation only. Otherwise Certbot reports: Client with the currently selected authenticator does not support any combination of challenges that will satisfy the CA.
Does *.MeineDomain.de also cover MeineDomain.de itself?
No. A wildcard applies only to the names exactly one level below. You have to request the bare domain in addition with another -d. It does not apply two levels down either, so not to a.b.MeineDomain.de.
Why do I need two TXT records with the same name?
Because the star and the bare domain are two separate validations that both land on _acme-challenge.MeineDomain.de, with different values. Both have to be in the zone at the same time. DNS standards permit this, but many provider interfaces replace the first value with the second.
How do I check whether the TXT record has propagated?
With dig against the authoritative nameserver and additionally against at least two public resolvers, for example dig +short TXT _acme-challenge.MeineDomain.de @1.1.1.1. Do not query the name before you have created the record, otherwise your resolver remembers the non-existence for the length of the negative TTL.
Does a manually issued wildcard certificate renew automatically?
No. Without a script Certbot reports on the automatic run: An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively, and skips the certificate. For unattended operation you need a DNS plugin for your provider or rfc2136.
Which Certbot DNS plugins are available on Debian 13?
Debian 13 ships only cloudflare, desec, google, infomaniak, rfc2136 and route53. Packages such as python3-certbot-dns-ovh, dns-linode or dns-digitalocean, which still exist on Debian 12 and Ubuntu, are no longer included there. Check this before you change distribution, otherwise renewal breaks unnoticed.
What does a CNAME delegation of _acme-challenge achieve?
It moves the TXT record into a separate zone. The server then needs no write permission on your main zone, so a compromised web server cannot change MX or A records. It also solves the problem of interfaces that do not allow two TXT values under the same name.

Lets-Encrypt Certbot Wildcard SSL Certificate DNS DNS-01 Debian Ubuntu