modified: README.md

modified:   certbot_dns_netbox/dns_netbox.py
This commit is contained in:
Sven Luthi committed 2026-10-26 18:37:14 +01:00
1 parent 91562ae538
commit 62b809db95
2 files changed
+66 -37

No files matched your search

+24 -13
View File
@@ -1,6 +1,6 @@
# certbot-dns-netbox
Certbot DNS-01 authenticator plugin that updates the challenge TXT record in
Certbot DNS-01 authenticator plugin that updates the challenge TXT records in
[NetBox DNS](https://github.com/sys4/netbox-plugin-dns) (the `netbox-dns`
NetBox plugin) via the bundled `nbapi.py` API helper.
@@ -13,16 +13,27 @@ and the plugin **never creates or deletes records**:
1. looks up the existing TXT records for the FQDN (records can be filtered by
`fqdn`, `zone` and `view` by name),
2. writes the challenge token into the existing record. If the record does
not exist yet, certbot aborts with an error: create the record once
beforehand (in the NetBox UI or your tooling) and rerun,
3. leaves the record in place after validation so it can be updated again on
the next renewal.
2. writes each challenge token into one of the pre-provisioned records,
claiming one record per token,
3. leaves the records in place after validation so they can be updated again
on the next renewal.
Because a single record holds a single token, a certificate request must not
combine a domain and its wildcard (`-d example.com -d '*.example.com'`): both
map to the same `_acme-challenge.example.com` record, and the second token
would overwrite the first. Issue those as separate certificates.
### Provisioning
The challenge TXT records must be created beforehand in netbox-dns, in the
view configured via `search_view` (`certbot_dns_netbox/dns_netbox.py`,
default: `global`). Each challenge token needs its own record:
| Certificate request | TXT records needed |
| ------------------------------------------------------ | ------------------ |
| `-d example.com` or `-d '*.example.com'` | 1 |
| `-d example.com -d '*.example.com'` (apex + wildcard) | 2 |
All records for a challenge share the same name (e.g. name
`_acme-challenge.unifi` in zone `ewcs.ch`); netbox-dns allows multiple TXT
records with different values for the same name. If a certificate request
needs more records than exist, certbot aborts with an error. Renewals simply
rewrite the values, so the records never need to be touched again.
Connection settings are taken from the environment, exactly as `nbapi.py`
expects them:
@@ -94,6 +105,6 @@ certbot certonly --dry-run \
-d sub.example.com # end-to-end against the staging ACME API
```
Watch the `_acme-challenge` TXT record in the `global` view: its value changes
to the challenge token during the run and remains in place afterwards. The
next renewal updates the same record again.
Watch the `_acme-challenge` TXT records in the `global` view: their values
change to the challenge tokens during the run and remain in place afterwards.
The next renewal updates the same records again.
+42 -24
View File
@@ -1,10 +1,11 @@
"""NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot.
The TXT record required for the ACME dns-01 challenge is managed
exclusively through the netbox-dns record API endpoint, via the bundled
nbapi.py helper. The zone endpoint is never queried and the plugin never
creates or deletes records: an existing TXT record (created beforehand in
NetBox) is updated with the challenge token and left in place afterwards.
The TXT records for the ACME dns-01 challenge are managed exclusively
through the netbox-dns record API endpoint, via the bundled nbapi.py
helper. The zone endpoint is never queried and the plugin never creates
or deletes records: the challenge TXT records (created beforehand in
NetBox, one per challenge token) are updated with the challenge tokens
and left in place afterwards.
Connection settings are taken from the environment, exactly as nbapi.py
expects them:
@@ -59,8 +60,8 @@ class Authenticator(dns_common.DNSAuthenticator):
def more_info(self) -> str:
return (
"This plugin updates an existing DNS TXT record in netbox-dns "
"with the token of the dns-01 challenge."
"This plugin updates pre-provisioned DNS TXT records in "
"netbox-dns with the tokens of the dns-01 challenges."
)
def _setup_credentials(self) -> None:
@@ -106,8 +107,8 @@ class _NetboxDnsClient:
Only the record API endpoint is used, so the API token needs no
permissions on zones. Records are never created or deleted: the
challenge TXT record must already exist in NetBox and is updated in
place.
challenge TXT records must be provisioned beforehand in NetBox, one
per challenge token, and are updated in place.
"""
def __init__(self, ttl: Optional[int] = None, insecure: bool = False):
@@ -118,6 +119,10 @@ class _NetboxDnsClient:
self._nbapi = nbapi
self._ttl = ttl
# record ids claimed by this certbot run; each challenge token gets
# its own pre-provisioned record so multiple tokens for the same
# name (e.g. apex and wildcard) do not overwrite each other.
self._claimed: set = set()
if insecure:
nbapi.disable_ssl_verify()
@@ -141,30 +146,43 @@ class _NetboxDnsClient:
) from e
def update_txt_record(self, record_fqdn: str, validation: str) -> None:
"""Write the challenge token into the existing TXT record."""
"""Write the challenge token into one of the pre-provisioned records.
Each challenge token claims its own record so that several tokens
for the same name (e.g. apex and wildcard on one certificate) stay
live simultaneously.
"""
fqdn = self._fqdn(record_fqdn)
records = self._get_txt_records(fqdn)
if any(record["value"] == validation for record in records):
logger.debug(
"TXT record for %s already has the expected value.", record_fqdn
)
return
for record in records:
if record["value"] == validation and not record.get("managed"):
self._claimed.add(record["id"])
logger.debug(
"TXT record for %s already has the expected value.",
record_fqdn,
)
return
existing = [record for record in records if not record.get("managed")]
if not existing:
existing = sorted(
(record for record in records if not record.get("managed")),
key=lambda record: record["id"],
)
free = [record for record in existing if record["id"] not in self._claimed]
if not free:
raise errors.PluginError(
f"No updatable TXT record found for {record_fqdn} in view "
f"{search_view!r}. This plugin only updates existing records: "
"create the record once (type TXT) and rerun certbot."
f"Not enough TXT records for {record_fqdn} in view "
f"{search_view!r}: {len(existing)} record(s) exist, but all of "
"them are already in use by this certificate request. Add "
"another TXT record with the same name and rerun certbot."
)
patch = {"id": existing[0]["id"], "value": validation}
target = free[0]
patch = {"id": target["id"], "value": validation}
if self._ttl:
patch["ttl"] = self._ttl
self._nbapi.modify_dns_record(patch)
self._claimed.add(target["id"])
logger.info(
"Updated TXT record %s (id %s).",
existing[0]["fqdn"],
existing[0]["id"],
"Updated TXT record %s (id %s).", target["fqdn"], target["id"]
)