certbot-dns-nexdns
Official Certbot DNS authenticator for NexDNS, a managed authoritative DNS platform with a REST API.
The plugin answers the ACME dns-01 challenge for you. Before validation it creates the _acme-challenge TXT record in your NexDNS zone, and after validation it removes it again, so issuing and renewing certificates needs no manual DNS edits.
Why DNS-01
- Wildcards. Let's Encrypt issues
*.example.comonly throughdns-01. No wildcard certificate can be obtained withhttp-01. - No inbound HTTP. Validation happens entirely over DNS, so nothing has to listen on port 80 or 443 and the host does not have to be reachable from the internet. You can issue for a machine behind NAT, for an internal service, or before the site is live.
- One certificate for the apex and everything under it, in a single run.
Requirements
- Python 3.9 or newer. The test matrix covers 3.9, 3.10, 3.11, 3.12 and 3.13
- Certbot 2.0 or newer
- A NexDNS account on a plan that includes API access, with the zone for the certificate's domain already hosted on it
- An API token carrying the three scopes listed below
requests 2.25 or newer is installed as a dependency.
Installation
pip install certbot-dns-nexdns
If Certbot itself was installed with pipx, add the plugin to that same environment instead, or Certbot will not find it:
pipx inject certbot certbot-dns-nexdns
Confirm that Certbot picked it up:
certbot plugins
dns-nexdns should appear in the list, described as "Obtain certificates using a DNS TXT record (if you are using NexDNS for DNS hosting)".
There is also a prebuilt image, nexdns/certbot, which is Certbot with this plugin already installed, for linux/amd64 and linux/arm64. See Docker.
API token
Create a token under Settings → API keys in the panel (nexdns.tech/settings/api-keys) and grant it exactly three scopes:
| Scope | What the plugin does with it |
|---|---|
zones.read |
Finds the zone that owns the challenge name, so _acme-challenge.shop.example.com is written into the example.com zone |
records.write |
Creates the challenge TXT record, and deletes it after validation |
records.read |
Reads the zone's TXT records back during cleanup, to find the exact record it created |
Nothing beyond these three is used. In particular the plugin never asks for zones.write, so a token scoped this way cannot create or delete zones.
Scopes are fixed when the key is created. To change them, create a new key and revoke the old one.
Credentials file
Certbot reads the token from an INI file:
mkdir -p ~/.nexdns && chmod 700 ~/.nexdns
cat > ~/.nexdns/certbot-credentials.ini <<'EOF'
dns_nexdns_token = nxd_0000000000000000000000000000000000000000
EOF
chmod 600 ~/.nexdns/certbot-credentials.ini
dns_nexdns_token is the only required key. When the file is world-readable Certbot logs Unsafe permissions on credentials configuration file and carries on regardless, so the warning is easy to miss in an unattended run. Set the mode yourself.
Issuing a certificate
Wildcard plus apex in one run
certbot certonly \
--authenticator dns-nexdns \
--dns-nexdns-credentials ~/.nexdns/certbot-credentials.ini \
--email admin@example.com \
--agree-tos \
--non-interactive \
-d example.com \
-d '*.example.com'
Quote *.example.com so the shell does not expand it. A wildcard does not cover the apex, which is why both names are listed, and it covers only one label: *.example.com matches www.example.com but not a.b.example.com.
Both names are validated at _acme-challenge.example.com, so this run creates two TXT records there and removes both afterwards.
Several domains on one certificate
certbot certonly \
--authenticator dns-nexdns \
--dns-nexdns-credentials ~/.nexdns/certbot-credentials.ini \
--email admin@example.com \
--agree-tos --non-interactive \
-d example.com -d www.example.com \
-d example.net -d '*.example.net'
Every zone named in one run must be hosted on the account the token belongs to, since the plugin uses that single token for all of them.
Propagation wait
--dns-nexdns-propagation-seconds is how long Certbot waits after the record is written before asking the ACME server to look for it. The default is 60. Do not lower it: at 30 seconds validation fails with NXDOMAIN. Raise it if your resolvers are slower to pick the record up.
certbot certonly \
--authenticator dns-nexdns \
--dns-nexdns-credentials ~/.nexdns/certbot-credentials.ini \
--dns-nexdns-propagation-seconds 120 \
-d '*.example.com'
Renewal
On the first successful issuance Certbot stores the authenticator and its options in the certificate's renewal configuration (/etc/letsencrypt/renewal/example.com.conf), so renewal takes no plugin flags:
certbot renew --dry-run # runs against the staging CA and issues nothing
certbot renew
Two things must still hold at renewal time: the credentials file has to exist at the path recorded in that configuration, and the token has to still be valid. A revoked or expired token is invisible until then, which is what makes a scheduled --dry-run worth having.
Certbot installed from PyPI does not schedule itself. Distribution packages usually ship a certbot.timer systemd unit or an /etc/cron.d/certbot entry; a pip or pipx install ships neither, so add one:
# /etc/cron.d/certbot
17 3,15 * * * root certbot renew --quiet
Twice a day is what Certbot's own documentation recommends. renew exits without doing anything unless a certificate is close to expiry, and an off-the-hour minute avoids the pile-up at 00.
Docker
The image inherits Certbot's entrypoint, so it is a drop-in replacement for certbot/certbot with the authenticator already present:
docker run --rm \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
-v ~/.nexdns/certbot-credentials.ini:/credentials.ini:ro \
nexdns/certbot certonly \
--authenticator dns-nexdns \
--dns-nexdns-credentials /credentials.ini \
--email admin@example.com \
--agree-tos --non-interactive \
-d example.com -d '*.example.com'
Tags are nexdns/certbot:latest and one per release, for example nexdns/certbot:1.0.0.
A renewal loop with Compose:
services:
certbot:
image: nexdns/certbot:latest
restart: unless-stopped
volumes:
- /etc/letsencrypt:/etc/letsencrypt
- /var/lib/letsencrypt:/var/lib/letsencrypt
- ./certbot-credentials.ini:/credentials.ini:ro
entrypoint: >
/bin/sh -c 'trap exit TERM; while :; do
certbot renew --quiet;
sleep 12h & wait $${!};
done'
$$ is how Compose escapes a literal $, so the container shell sees wait ${!} and waits on the backgrounded sleep. That is what makes docker compose down stop the container at once instead of up to twelve hours later.
Choosing the API base URL
The plugin talks to https://api.nexdns.tech/v1. Set dns_nexdns_api_url in the credentials file to point it at a different base URL:
# ~/.nexdns/certbot-credentials.ini
dns_nexdns_token = nxd_0000000000000000000000000000000000000000
dns_nexdns_api_url = https://api.nexdns.tech/v1
The value is the full base URL including the API version segment. A trailing slash is ignored. Omit the key entirely to get the default, which is the value shown above.
Options
Command-line options, as reported by certbot --help dns-nexdns:
| Option | Default | Description |
|---|---|---|
--dns-nexdns-credentials |
none | Path to the credentials INI file. Certbot prompts for it interactively when it is not supplied, and stores the resolved absolute path |
--dns-nexdns-propagation-seconds |
60 |
Seconds to wait for the TXT record to become visible before the ACME server is asked to verify it |
Credentials file keys:
| Key | Default | Description |
|---|---|---|
dns_nexdns_token |
none, required | API token, in the form nxd_… |
dns_nexdns_api_url |
https://api.nexdns.tech/v1 |
API base URL |
What happens during a challenge
- Certbot asks the plugin to publish a value at
_acme-challenge.<domain>. - The plugin looks up the zone by trying the longest suffix of that name first and shortening it label by label, so
_acme-challenge.shop.example.comlands in a delegatedshop.example.comzone when the account has one, and inexample.comotherwise. - It creates the TXT record with a TTL of 120 seconds. Certbot then waits out
--dns-nexdns-propagation-seconds. - After validation the plugin lists the zone's TXT records, matches its own value exactly and deletes that one record. Other TXT records at the same name are left untouched.
- If cleanup fails it logs a warning instead of failing the run, because the certificate has already been issued by that point. The leftover record stays in the zone until you remove it.
Every API call carries a 30-second timeout, so an unreachable API fails the run rather than hanging an unattended certbot renew indefinitely.
Troubleshooting
API failures reach you through Certbot as Failed to add TXT record: NexDNS API error: <message>, where the message comes from the API and names the cause.
NexDNS API error: Invalid API key.
The value of dns_nexdns_token matches no key on the account. A truncated paste is the usual reason; tokens begin with nxd_. A key that has been revoked in the panel reports the same thing.
NexDNS API error: API key has expired.
The key was created with an expiry date that has passed. Create a replacement and update the credentials file. The renewal configuration keeps pointing at the same file, so nothing else changes.
NexDNS API error: API access is not included in your plan.
The key is valid, but the account's plan does not include API access. See pricing. This is checked on every request, so it can appear on a key that worked before a plan change.
NexDNS API error: API key lacks the "records.write" permission.
The key is missing a scope. It needs all three of zones.read, records.read and records.write. Since scopes cannot be edited, create a new key with all three and revoke the old one. A key created with no scopes at all reports API key has no permissions assigned instead.
NexDNS API error: Too many requests. Please retry after …
The account's API rate limit was reached. The response carries a Retry-After header. This is worth checking if you drive many certificates through one token in a tight loop.
Failed to add TXT record: Zone not found for _acme-challenge.example.com.
No zone on the token's account matches any suffix of that name. nexdns zone list from the CLI shows what the account actually holds. A certificate for shop.example.com needs either the example.com zone or a shop.example.com zone of its own to be present there.
Missing property in credentials configuration file …: Property "dns_nexdns_token" not found
The INI file exists but has no dns_nexdns_token line, or its value is empty. Key names are exact, including the dns_nexdns_ prefix.
File not found: …
--dns-nexdns-credentials points at a path that does not exist. Certbot resolves the path to an absolute one at issuance and records that in the renewal configuration, so a credentials file that is later moved or deleted fails at renewal rather than at issuance.
No TXT record found at _acme-challenge.example.com, reported by the ACME server
The record was written, but the ACME server did not see it. Two causes actually produce this:
- The wait was too short. Keep
--dns-nexdns-propagation-secondsat 60 or raise it. - The domain is not delegated to the NexDNS nameservers, so the record is published in a zone nobody queries for that name. Check the delegation with
dig NS example.com.
To watch the record appear, run this from a second terminal while Certbot is in its propagation wait, since the record is cleaned up as soon as the attempt ends:
dig +short TXT _acme-challenge.example.com @1.1.1.1
Certbot writes the full detail of any failure to /var/log/letsencrypt/letsencrypt.log. Reproduce with --dry-run -v, which runs against the staging CA and so does not spend production issuance quota on a failing attempt.
Development
git clone https://github.com/nexdns/certbot-dns-nexdns
cd certbot-dns-nexdns
pip install -e .[dev] certbot
python -m pytest
The unit tests mock the HTTP session, so they need neither an account nor network access.
Installing is not the same as being loadable: Certbot discovers plugins through entry points, and a load failure is reported to the user as this plugin's bug. Check discovery the way CI does:
certbot plugins 2>&1 | grep dns-nexdns
For end-to-end work, issue against Let's Encrypt's staging CA. Its certificates are untrusted, which is the point, and a failing run there costs no production issuance quota:
certbot certonly \
--server https://acme-staging-v02.api.letsencrypt.org/directory \
--authenticator dns-nexdns \
--dns-nexdns-credentials ~/.nexdns/certbot-credentials.ini \
-d '*.example.com'
Links
- PyPI: pypi.org/project/certbot-dns-nexdns
- Docker Hub: hub.docker.com/r/nexdns/certbot
- ACME guide: nexdns.tech/docs/acme
- NexDNS: nexdns.tech
- Certbot documentation: certbot.eff.org
- CLI: github.com/nexdns/cli
- Terraform provider: github.com/nexdns/terraform-provider-nexdns
- OctoDNS provider: github.com/nexdns/octodns-nexdns
- Issues: github.com/nexdns/certbot-dns-nexdns/issues
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file certbot_dns_nexdns-1.0.1.tar.gz.
File metadata
- Download URL: certbot_dns_nexdns-1.0.1.tar.gz
- Upload date:
- Size: 19.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7aef5180db6b90cacf19ac994975412203791e2132f0d08894a87fe17a7f65e0
|
|
| MD5 |
b5b5448d108a38d79c4c11b358e6ee8d
|
|
| BLAKE2b-256 |
4177eb697abff338f2e8aee81251fbc59a4ec9b24cb596d2a821c769636d5af7
|
Provenance
The following attestation bundles were made for certbot_dns_nexdns-1.0.1.tar.gz:
Publisher:
publish.yml on nexdns/certbot-dns-nexdns
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
certbot_dns_nexdns-1.0.1.tar.gz -
Subject digest:
7aef5180db6b90cacf19ac994975412203791e2132f0d08894a87fe17a7f65e0 - Sigstore transparency entry: 2310277759
- Sigstore integration time:
-
Permalink:
nexdns/certbot-dns-nexdns@5952bc661d269087a9253f9ca95b5392174b596d -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/nexdns
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5952bc661d269087a9253f9ca95b5392174b596d -
Trigger Event:
push
-
Statement type:
File details
Details for the file certbot_dns_nexdns-1.0.1-py3-none-any.whl.
File metadata
- Download URL: certbot_dns_nexdns-1.0.1-py3-none-any.whl
- Upload date:
- Size: 15.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5716c00b9101002d6a667211673ebbb7fce714fd698ef445d794f6abb159c378
|
|
| MD5 |
38470bebee5c720e3cc186c4c3ee28f2
|
|
| BLAKE2b-256 |
2afd286a8a5f0cd105f92926fddc0e8d7488f3326245b0d1d2d8de1e12e1f6fb
|
Provenance
The following attestation bundles were made for certbot_dns_nexdns-1.0.1-py3-none-any.whl:
Publisher:
publish.yml on nexdns/certbot-dns-nexdns
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
certbot_dns_nexdns-1.0.1-py3-none-any.whl -
Subject digest:
5716c00b9101002d6a667211673ebbb7fce714fd698ef445d794f6abb159c378 - Sigstore transparency entry: 2310277766
- Sigstore integration time:
-
Permalink:
nexdns/certbot-dns-nexdns@5952bc661d269087a9253f9ca95b5392174b596d -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/nexdns
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5952bc661d269087a9253f9ca95b5392174b596d -
Trigger Event:
push
-
Statement type: