How to issue a wildcard certificate with Certbot

A wildcard certificate lets one TLS certificate cover every first-level host under a domain, but Certbot cannot prove a wildcard name with the HTTP or standalone challenges. The certificate authority must see a DNS-01 TXT record for the domain, so the DNS provider account and its API token become part of the issuance path.

Using a Certbot DNS plugin keeps that TXT record lifecycle inside Certbot. The plugin creates the _acme-challenge record, waits for DNS propagation, asks the ACME server to validate it, and removes the record after the order finishes.

The package names use the Debian and Ubuntu APT layout with the Cloudflare DNS plugin. Use the matching DNS plugin and credential option for another provider, and request both example.com and *.example.com when the apex name must also be covered because a wildcard name does not match the bare domain.

Steps to issue a wildcard certificate with Certbot:

  1. Open a terminal on the host that will store the certificate files.
  2. Install Certbot, the Cloudflare DNS plugin, and DNS lookup tools.
    $ sudo apt install --assume-yes certbot python3-certbot-dns-cloudflare dnsutils
    Reading package lists... Done
    Building dependency tree... Done
    The following NEW packages will be installed:
      certbot dnsutils python3-certbot-dns-cloudflare python3-cloudflare
    ##### snipped #####
    Setting up python3-certbot-dns-cloudflare ...

    For another DNS provider, install that provider's Certbot DNS plugin and use its matching credential flag in the later commands.

  3. Confirm that the zone is delegated to the DNS provider handled by the plugin.
    $ host -t NS example.com
    example.com name server arvind.ns.cloudflare.com.
    example.com name server nina.ns.cloudflare.com.

    The Cloudflare plugin can only update zones that are active in Cloudflare DNS. Use the provider-specific Certbot DNS plugin that matches the authoritative nameservers for the zone.

  4. Create a root-owned credential file for the DNS plugin.
    $ sudo install -m 600 -o root -g root /dev/null /etc/letsencrypt/cloudflare.ini
  5. Add a DNS API token that can edit the zone's TXT records.
    $ sudoedit /etc/letsencrypt/cloudflare.ini
    dns_cloudflare_api_token = <cloudflare-api-token>

    Use a token scoped to the needed zone and DNS record edits. A broad DNS account token can change unrelated records if the host is compromised.

  6. Confirm that only root can read the credential file.
    $ sudo ls -l /etc/letsencrypt/cloudflare.ini
    -rw------- 1 root root 67 Jun 12 09:20 /etc/letsencrypt/cloudflare.ini
  7. Confirm that Certbot can load the DNS plugin.
    $ certbot plugins
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    ##### snipped #####
    * dns-cloudflare
    Description: Obtain certificates using a DNS TXT record (if you are using Cloudflare for DNS).
    Interfaces: Authenticator, Plugin
    ##### snipped #####
  8. Test the wildcard order against the staging ACME service.
    $ sudo certbot certonly \
      --dns-cloudflare \
      --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
      --dns-cloudflare-propagation-seconds 60 \
      --dry-run \
      --non-interactive \
      --agree-tos \
      --email admin@example.com \
      --no-eff-email \
      -d example.com \
      -d '*.example.com'
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Simulating a certificate request for example.com and *.example.com
    Waiting 60 seconds for DNS changes to propagate
    The dry run was successful.

    Quote the wildcard name so the local shell does not expand *.example.com before Certbot receives it.

  9. Request the production wildcard certificate.
    $ sudo certbot certonly \
      --dns-cloudflare \
      --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
      --dns-cloudflare-propagation-seconds 60 \
      --cert-name example.com-wildcard \
      --non-interactive \
      --agree-tos \
      --email admin@example.com \
      --no-eff-email \
      -d example.com \
      -d '*.example.com'
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Requesting a certificate for example.com and *.example.com
    Waiting 60 seconds for DNS changes to propagate
    Successfully received certificate.
    Certificate is saved at: /etc/letsencrypt/live/example.com-wildcard/fullchain.pem
    Key is saved at:         /etc/letsencrypt/live/example.com-wildcard/privkey.pem

    example.com covers the bare domain, while *.example.com covers names such as app.example.com. The wildcard does not cover deeper names such as api.dev.example.com.

  10. Confirm that Certbot saved the wildcard certificate lineage.
    $ sudo certbot certificates --cert-name example.com-wildcard
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Found the following certs:
      Certificate Name: example.com-wildcard
        Domains: *.example.com example.com
        Expiry Date: 2026-09-10 14:20:00+00:00 (VALID: 89 days)
        Certificate Path: /etc/letsencrypt/live/example.com-wildcard/fullchain.pem
        Private Key Path: /etc/letsencrypt/live/example.com-wildcard/privkey.pem

    Use these paths in the web server, load balancer, mail server, or other TLS service that will present the certificate.

  11. Test unattended renewal for the saved certificate.
    $ sudo certbot renew --cert-name example.com-wildcard --dry-run
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Processing /etc/letsencrypt/renewal/example.com-wildcard.conf
    Simulating renewal of an existing certificate for example.com and *.example.com
    Congratulations, all simulated renewals succeeded:
      /etc/letsencrypt/live/example.com-wildcard/fullchain.pem (success)