new file: .gitea/workflows/publish.yml
Publish Python package to PyPI / publish (push) Successful in 29s

new file:   .github/workflows/python-publish.yml
new file:   .gitignore
new file:   LICENSE
modified:   README.md
modified:   certbot_dns_netbox/dns_netbox.py
modified:   pyproject.toml
This commit is contained in:
Sven Luthi committed 2026-10-27 10:29:19 +01:00
1 parent 91562ae538
commit f1e7737a07
7 files changed
+189 -40

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
name: Publish Python package to PyPI
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: self-hosted
steps:
- name: Check out repository
uses: https://github.com/actions/checkout@v4
- name: Create virtual environment
run: python3 -m venv .venv
- name: Install build tools
run: .venv/bin/pip install --upgrade build twine
- name: Verify tag matches package version
run: |
TAG="${GITHUB_REF_NAME}"
VERSION="$(grep -m1 '^version = ' pyproject.toml | cut -d '"' -f2)"
if [ "$TAG" != "v$VERSION" ]; then
echo "::error::Tag ${TAG} does not match package version v${VERSION}"
exit 1
fi
- name: Build package
run: .venv/bin/python3 -m build
- name: Check package
run: .venv/bin/python3 -m twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
TWINE_NON_INTERACTIVE: "1"
run: .venv/bin/python3 -m twine upload dist/*
+42
View File
@@ -0,0 +1,42 @@
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Create and Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
build-and-publish:
name: Build and Publish
environment: pypi
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
#- uses: actions/setup-python@v5
# with:
# python-version: "3.x"
- name: Install build tools
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade build
- name: Build release
run: |
python -m build --sdist --wheel
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+6
View File
@@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
build/
dist/
*.egg-info/
.venv/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sven Luthi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+27 -14
View File
@@ -1,9 +1,11 @@
# certbot-dns-netbox
# 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.
***DISCLAIMER: THIS PROJECT IS VIBE CODED USING AN LLM.***
## How it works
For every domain in the certificate request certbot asks this plugin to set an
@@ -13,16 +15,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 +107,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"]
)
+9 -2
View File
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=61"]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[project]
@@ -8,7 +8,9 @@ version = "1.0.0"
description = "NetBox DNS (netbox-plugin-dns) DNS-01 authenticator plugin for Certbot"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "Apache-2.0" }
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "Sven Luthi", email = "dev@sven.luethi.co" }]
keywords = ["certbot", "acme", "letsencrypt", "netbox", "dns", "dns-01"]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -16,6 +18,7 @@ classifiers = [
"Intended Audience :: System Administrators",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Security",
]
@@ -25,6 +28,10 @@ dependencies = [
"urllib3>=1.26",
]
[project.urls]
Homepage = "https://git.luethi.co/sven/certbot-dns-netbox"
Repository = "https://git.luethi.co/sven/certbot-dns-netbox.git"
[project.entry-points."certbot.plugins"]
dns-netbox = "certbot_dns_netbox.dns_netbox:Authenticator"