# Certbot wildcard certificate with the Cloudflare DNS plugin

> Get a Let's Encrypt wildcard certificate with Certbot's Cloudflare DNS plugin: scoped API token, credentials file, DNS-01 issue, renewal with reload.

Source: https://ssl.sh/guides/certbot-dns-cloudflare-wildcard/
Last updated: 2026-11-10

Certbot's Cloudflare DNS plugin gets a Let's Encrypt wildcard certificate for `example.com` and `*.example.com` by creating DNS-01 TXT records with a scoped Cloudflare API token. Renewal reuses the saved settings, and a deploy hook reloads the web server.

The steps apply to Linux servers with Certbot 4.1 or later installed from snap (or pip) and DNS hosted on Cloudflare.

## Prerequisites

DNS-01 needs no inbound ports, only a Cloudflare API token for the zone.

- A Linux server with systemd, snapd and sudo access.
- The domain's nameservers on Cloudflare (the zone shows as **Active**).
- A Cloudflare API token created under **My Profile → API Tokens** with the permission **Zone → DNS → Edit**, limited to `example.com`.
- Outbound HTTPS to Let's Encrypt and `api.cloudflare.com`.

## Step 1: Install Certbot and the Cloudflare plugin

The Cloudflare plugin is a separate snap that Certbot loads once you allow plugins to run as root:

```bash
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare
certbot plugins | grep -A1 dns-cloudflare
```

With a pip install in `/opt/certbot`, run `sudo /opt/certbot/bin/pip install certbot-dns-cloudflare` instead.

## Step 2: Store the API token in a credentials file

Certbot reads the token from an INI file that only root can read:

```bash
sudo mkdir -p /root/.secrets/certbot
sudo tee /root/.secrets/certbot/cloudflare.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = your-cloudflare-api-token
EOF
sudo chmod 600 /root/.secrets/certbot/cloudflare.ini
```

Certbot prints a warning if other users on the system can read the file.

## Step 3: Issue the wildcard certificate

Request the apex and the wildcard in one certificate:

```bash
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
  -d example.com -d '*.example.com'
```

Quote `'*.example.com'` so the shell does not expand it. Including `example.com` matters because the wildcard does not cover the bare domain. The files land in `/etc/letsencrypt/live/example.com/`.

## Step 4: Use the certificate and add a reload hook

Point the web server at `fullchain.pem` and `privkey.pem`, for example in Nginx:

```nginx
ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
```

Because `certonly` does not touch the server configuration, add a deploy hook so renewals reload it:

```bash
sudo certbot reconfigure --cert-name example.com --deploy-hook "systemctl reload nginx"
```

Alternatively, `-i nginx` on the issue command lets the Nginx installer edit and reload Nginx for you.

## Verify

The apex and every subdomain should present the same certificate, with `example.com` and `*.example.com` in its SAN list. Check them with the [SSL checker](https://ssl.sh/tools/ssl-checker/), or from a terminal:

```bash
curl ssl.sh/example.com
curl ssl.sh/api.example.com
```

On the server:

```bash
sudo certbot certificates
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -ext subjectAltName
```

## Automatic renewal

The plugin name, credentials path and propagation delay are saved in `/etc/letsencrypt/renewal/example.com.conf`, so the snap timer renews the wildcard twice a day as needed, following Let's Encrypt's ARI window on Certbot 4.1 and later. Test it:

```bash
sudo certbot renew --dry-run
systemctl list-timers | grep certbot
```

Keep the token valid: if you rotate or delete it, update the credentials file or renewals will fail silently until expiry. Add the domain to [ssl.sh monitoring](https://ssl.sh/monitor/) (in development) because Let's Encrypt no longer emails before expiry. For hook details, see [Certbot renewal hooks](https://ssl.sh/guides/certbot-renewal-hooks/).

## Troubleshooting

- **Error determining zone_id** or **9109: Invalid access token**: the token is wrong, expired or not scoped to this zone; create a new token with Zone → DNS → Edit.
- **Unsafe permissions on credentials configuration file**: run `sudo chmod 600` on the ini file.
- **DNS problem: NXDOMAIN looking up TXT**: propagation was too slow; add `--dns-cloudflare-propagation-seconds 60`.
- **CAA record prevents issuance**: a CAA record without `letsencrypt.org`, or an `issuewild` record that excludes it; see [CAA record prevents issuance](https://ssl.sh/errors/caa-record-prevents-issuance/).
- **too many certificates already issued**: use `--dry-run` or `--test-cert` while testing; see [Let's Encrypt rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/).
- **Subdomain shows a name mismatch**: names two levels deep (`a.b.example.com`) are not covered by `*.example.com`; see [NET::ERR_CERT_COMMON_NAME_INVALID](https://ssl.sh/errors/net-err-cert-common-name-invalid/).
