Skip to main content

certbot-dns-nexdns

PyPI Python versions Docker image License

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.com only through dns-01. No wildcard certificate can be obtained with http-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

  1. Certbot asks the plugin to publish a value at _acme-challenge.<domain>.
  2. 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.com lands in a delegated shop.example.com zone when the account has one, and in example.com otherwise.
  3. It creates the TXT record with a TTL of 120 seconds. Certbot then waits out --dns-nexdns-propagation-seconds.
  4. 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.
  5. 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-seconds at 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

License

Apache-2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

certbot_dns_nexdns-1.0.1.tar.gz (19.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

certbot_dns_nexdns-1.0.1-py3-none-any.whl (15.1 kB view details)

Uploaded Python 3

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

Hashes for certbot_dns_nexdns-1.0.1.tar.gz
Algorithm Hash digest
SHA256 7aef5180db6b90cacf19ac994975412203791e2132f0d08894a87fe17a7f65e0
MD5 b5b5448d108a38d79c4c11b358e6ee8d
BLAKE2b-256 4177eb697abff338f2e8aee81251fbc59a4ec9b24cb596d2a821c769636d5af7

See more details on using hashes here.

Provenance

The following attestation bundles were made for certbot_dns_nexdns-1.0.1.tar.gz:

Publisher: publish.yml on nexdns/certbot-dns-nexdns

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file certbot_dns_nexdns-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for certbot_dns_nexdns-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5716c00b9101002d6a667211673ebbb7fce714fd698ef445d794f6abb159c378
MD5 38470bebee5c720e3cc186c4c3ee28f2
BLAKE2b-256 2afd286a8a5f0cd105f92926fddc0e8d7488f3326245b0d1d2d8de1e12e1f6fb

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page