A wildcard certificate for *.example.com is valid for every first-level subdomain (app.example.com, api.example.com and so on), so new subdomains work over HTTPS without issuing a new certificate each time. Let's Encrypt issues wildcard certificates only through the DNS-01 challenge, where you prove control of the domain by publishing a TXT record. In this tutorial you will use Certbot and its Cloudflare DNS plugin on Ubuntu 24.04 to obtain a certificate for your_domain and *.your_domain, set up unattended renewal, and serve it from Nginx.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, and a non-root user with sudo privileges.
  • A registered domain whose DNS is hosted on Cloudflare. Throughout the guide, replace your_domain with it. Other DNS providers work with their own plugin; see Step 1.
  • DNS records pointing to your server: an A record for your_domain and an A record for * (the wildcard), both with your server's public IP your_server_ip.

A wildcard only covers one label: *.your_domain matches app.your_domain but not your_domain itself nor dev.app.your_domain. That is why this guide requests both your_domain and *.your_domain.

Step 1 - Installing Certbot and the DNS plugin

The DNS challenge requires Certbot to create a TXT record through your DNS provider's API, which is what DNS plugins do. Install Certbot, the Cloudflare plugin and Nginx (which will serve the certificate in Step 5) from the Ubuntu repositories:

sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare nginx

Allow HTTP and HTTPS through UFW:

sudo ufw allow 'Nginx Full'

Confirm the plugin is available:

sudo certbot plugins
* dns-cloudflare
Description: Obtain certificates using a DNS TXT record (if you are using
Cloudflare for DNS).
...

If your DNS is hosted elsewhere, Ubuntu 24.04 packages plugins for several other providers. The rest of the guide is the same; only the plugin flag and the credentials file change:

DNS providerPackageCertbot flag
Cloudflarepython3-certbot-dns-cloudflare--dns-cloudflare
AWS Route 53python3-certbot-dns-route53--dns-route53
DigitalOceanpython3-certbot-dns-digitalocean--dns-digitalocean
OVHcloudpython3-certbot-dns-ovh--dns-ovh
Your own BIND/PowerDNS (RFC 2136)python3-certbot-dns-rfc2136--dns-rfc2136

Step 2 - Creating a Cloudflare API token

Give Certbot a token that can only edit DNS in one zone, not your whole Cloudflare account. In the Cloudflare dashboard:

  1. Open My Profile > API Tokens and click Create Token.
  2. Use the Edit zone DNS template.
  3. Under Zone Resources, select Include > Specific zone > your_domain.
  4. Create the token and copy it. Cloudflare shows it only once.

Store the token in a file that only root can read. Certbot runs as root, including during automatic renewal, so keep it under /etc/letsencrypt:

sudo nano /etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = your_cloudflare_api_token

Restrict its permissions. Certbot warns if other users can read the file:

sudo chmod 600 /etc/letsencrypt/cloudflare.ini
sudo ls -l /etc/letsencrypt/cloudflare.ini
-rw------- 1 root root 55 Sep 25 10:12 /etc/letsencrypt/cloudflare.ini

Step 3 - Requesting the wildcard certificate

Run the request against the Let's Encrypt staging environment first with --dry-run. It goes through the full DNS challenge without issuing a real certificate, so mistakes do not count against the production rate limits:

sudo certbot certonly --dry-run \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d your_domain -d '*.your_domain'

The first time you run Certbot it asks for an email address for expiry notices and for acceptance of the terms of service. A successful dry run ends with:

The dry run was successful.

Now request the real certificate. The --deploy-hook is saved in the renewal configuration and reloads Nginx each time the certificate is renewed:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d your_domain -d '*.your_domain' \
  --deploy-hook "systemctl reload nginx"

Quote '*.your_domain' so the shell does not try to expand the asterisk. Certbot creates a _acme-challenge.your_domain TXT record, waits 30 seconds for it to propagate, lets Let's Encrypt validate it, and then removes it:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/your_domain/privkey.pem
This certificate expires on ....
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

List the certificates Certbot manages to confirm that both names are included:

sudo certbot certificates
Found the following certs:
  Certificate Name: your_domain
    Serial Number: 4a3f...
    Key Type: ECDSA
    Domains: your_domain *.your_domain
    Expiry Date: ... (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/your_domain/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/your_domain/privkey.pem

Step 4 - Verifying automatic renewal

Let's Encrypt certificates are short-lived, so renewal must be automatic. The Ubuntu certbot package installs a systemd timer that runs certbot renew twice a day and renews any certificate close to expiry, reusing the plugin and credentials saved in /etc/letsencrypt/renewal/your_domain.conf. Check that the timer is active:

systemctl list-timers certbot.timer
NEXT                        LEFT     LAST  PASSED  UNIT          ACTIVATES
Thu 2026-09-25 21:47:00 UTC 11h left -     -       certbot.timer certbot.service

Simulate a renewal to confirm the stored configuration works end to end:

sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/your_domain/fullchain.pem (success)

If the dry run fails, fix it now: the most common cause is a token that was revoked or lacks DNS edit permission for the zone.

Step 5 - Using the certificate in Nginx

With one certificate covering every subdomain, a single Nginx server block can answer for all of them. Create a server block:

sudo nano /etc/nginx/sites-available/your_domain
server {
    listen 80;
    listen [::]:80;
    server_name your_domain *.your_domain;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your_domain *.your_domain;

    ssl_certificate     /etc/letsencrypt/live/your_domain/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    root /var/www/html;
    index index.html index.nginx-debian.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

The redirect uses $host rather than $server_name, so a request for app.your_domain is redirected to the same subdomain and not to the first name in the list. Always point ssl_certificate at fullchain.pem, which contains your certificate plus the intermediate; cert.pem alone causes chain errors on many clients.

Enable the site, test the configuration and reload Nginx:

sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Test a subdomain that you never configured explicitly. Thanks to the wildcard DNS record and the wildcard certificate, it works over HTTPS:

curl -sI https://anything.your_domain | head -n 1
HTTP/2 200

Check which names the served certificate covers:

echo | openssl s_client -connect your_server_ip:443 -servername anything.your_domain 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN=your_domain
X509v3 Subject Alternative Name:
    DNS:*.your_domain, DNS:your_domain

In a real deployment you would usually add separate server blocks for specific subdomains (for example api.your_domain proxying to an application) that reuse the same two ssl_certificate lines.

Troubleshooting

  • Unable to determine zone identifier for your_domain. The token does not have access to that zone. Recreate it with Zone Resources set to the correct zone.
  • Incorrect TXT record ... found at _acme-challenge.your_domain. Validation ran before the record propagated. Increase --dns-cloudflare-propagation-seconds (for example to 60). You can watch the record during a run with dig +short TXT _acme-challenge.your_domain @1.1.1.1.
  • Renewal succeeds but browsers still see the old certificate. Nginx was not reloaded. Check that renew_hook = systemctl reload nginx appears in /etc/letsencrypt/renewal/your_domain.conf, or add a script to /etc/letsencrypt/renewal-hooks/deploy/.
  • too many certificates already issued. You hit a Let's Encrypt rate limit by repeating real requests. Wait for the limit window to pass and use --dry-run for testing.
  • Your DNS provider has no plugin. certbot certonly --manual --preferred-challenges dns lets you create the TXT record by hand, but such certificates cannot renew unattended. Moving DNS to a provider with an API is the better fix.

Conclusion

You obtained a Let's Encrypt certificate for your_domain and *.your_domain using the DNS-01 challenge, verified that Certbot renews it automatically and reloads Nginx, and served every subdomain over HTTPS with a single certificate. Next, you can add server blocks that proxy individual subdomains to applications, enable HSTS once all subdomains are HTTPS-only, and monitor expiry dates with your usual alerting as a safety net.