FENO DNS Authenticator plugin for Certbot.
This plugin automates the process of completing a dns-01 challenge by creating, and
subsequently removing, TXT records using the FENO public API — for
.no domains registered with FENO and hosted on FENO nameservers. DNS-01 is the only challenge
type that issues wildcard certificates.
Zero-install alternative: RFC2136. Certbot ships
--dns-rfc2136out of the box and it works against FENO's TSIG-authenticated DNS UPDATE gateway atddns.feno.no:53. If your host can reach port 53 outbound, that path needs no third-party plugin at all — see Using certbot's built-in--dns-rfc2136instead below. Use this plugin when port 53 is blocked, or when you prefer anacme:writeAPI key over a TSIG key.
The FENO public API is rolled out per account. The instructions below apply when FENO's public API is enabled on your account and you can create API keys in the FENO dashboard.
| Argument | Description |
|---|---|
--dns-feno-credentials |
FENO credentials INI file. (required) |
--dns-feno-propagation-seconds |
Seconds to wait for DNS to propagate before asking the ACME server to verify the DNS record. (Default: 60) |
pip install "git+https://github.com/mrerikcodes/certbot-dns-feno@v0.1.1"PyPI publication pending; the PyPI name will be
certbot-dns-feno. Until then install from the tagged Git release above.
Install it into the same Python environment as certbot. If certbot came from a distribution package or a snap, that environment is not the system one:
# certbot installed with pip into a venv (the certbot.eff.org recommended layout)
sudo /opt/certbot/bin/pip install "git+https://github.com/mrerikcodes/certbot-dns-feno@v0.1.1"
# certbot installed as a snap: third-party plugins are installed as snaps, which this project
# does not ship — use the RFC2136 path below, or a pip-based certbot.Verify that certbot sees the plugin:
certbot plugins
# ...
# * dns-feno
# Description: Obtain certificates using a DNS TXT record (if you are using FENO for DNS).Create a public API key in the FENO dashboard (Konto → API keys). Give it the acme:write
scope and nothing more: an acme:write key can create and delete TXT records at
_acme-challenge / _acme-challenge.<label> and cannot touch any other record, so a leaked
renewal credential on a build agent cannot redirect your mail or delete your A records. The key
is shown once and looks like feno_live_….
# /etc/letsencrypt/feno.ini
dns_feno_api_key = feno_live_0123456789abcdef0123456789abcdef
# Optional — only for a non-production API host. Include the version prefix.
# dns_feno_api_base = https://api.feno.no/v1sudo chmod 600 /etc/letsencrypt/feno.iniThe path to this file can be provided interactively or with --dns-feno-credentials. Certbot
records the path for use during renewal, but does not store the file's contents.
Caution: protect this file as you would the password to your FENO account. Certbot warns ("Unsafe permissions on credentials configuration file") on every run, renewals included, if other users can read it — fix it with
chmod 600.
Acquire a certificate for kunde.no and www.kunde.no, waiting the default 60 seconds for
propagation:
sudo certbot certonly \
--authenticator dns-feno \
--dns-feno-credentials /etc/letsencrypt/feno.ini \
-d kunde.no -d www.kunde.noA wildcard does not cover the apex — *.kunde.no matches www.kunde.no but not
kunde.no itself — so the certificate you almost always want names both:
sudo certbot certonly \
--authenticator dns-feno \
--dns-feno-credentials /etc/letsencrypt/feno.ini \
--dns-feno-propagation-seconds 60 \
-d kunde.no -d '*.kunde.no'During issuance two TXT records appear at _acme-challenge.kunde.no, one per
authorization. That is normal. The plugin only ever deletes the record whose value it published,
so neither challenge deletes the other.
Certbot reuses the plugin and the credentials file automatically:
sudo certbot renew --dry-run- certbot hands the plugin the domain being validated (for
*.kunde.nothat iskunde.no), the challenge name (_acme-challenge.kunde.no) and the token. - The plugin finds the zone FENO hosts by peeling labels off the domain one at a time and
asking
GET /v1/domains/{candidate}— the first200is the zone..noholds both plain second-level names and delegated sub-zones, so the zone is never guessed from a label count.404 DOMAIN_NOT_MANAGEDmoves on to the parent (over an API key FENO answers the same 404 whether the name is unknown or belongs to another account, on purpose);401, an IP allow-list refusal or a suspended account stop immediately with a clear error, because a bad key would otherwise look exactly like "domain not found" all the way up to the TLD. Anacme:writekey is allowed to make this call — it gets a minimal{domain, status, fenoNameservers}answer — and iffenoNameserversisfalsethe plugin stops there with a message saying the domain is delegated elsewhere.- A key holding neither
acme:writenordomains:read(for exampledns:writealone) cannot read domains at all (403 INSUFFICIENT_SCOPE). The plugin then derives the zone from public DNS instead — an SOA walk with dnspython — and carries on. No configuration needed.
- A key holding neither
- It
POSTs a TXT record to/v1/domains/{zone}/dnswith the name relative to the zone (_acme-challenge, or_acme-challenge.www), TTL 60, and remembers the record id. - certbot waits
--dns-feno-propagation-seconds(FENO's API acknowledges when the write is accepted, not when every anycast nameserver serves it), then lets the CA validate. - Cleanup deletes the remembered record by id. If the id is not known (it always is within one
certbot run), the plugin lists
GET /v1/domains/{zone}/dns— anacme:writekey sees only the_acme-challenge*TXT records, which is all it needs — and deletes only the TXT record whose name and value match. Cleanup failures are logged, never fatal — a leftover challenge TXT record is harmless.
429 responses are retried a bounded number of times, honouring Retry-After.
FENO runs an RFC 2136 DNS UPDATE gateway (ddns.feno.no, port 53 — UDP and TCP must be
open outbound, TSIG hmac-sha256). Create a TSIG key yourself in the dashboard (API keys →
DNS update keys); it is acme_only by default, which can only write _acme-challenge TXT
records. The response includes a ready-made certbot credentials file.
sudo tee /etc/letsencrypt/feno-rfc2136.ini >/dev/null <<'INI'
dns_rfc2136_server = ddns.feno.no
dns_rfc2136_port = 53
dns_rfc2136_name = acme-kunde-no.key.feno.no.
dns_rfc2136_secret = BASE64_SECRET_FROM_FENO
dns_rfc2136_algorithm = HMAC-SHA256
INI
sudo chmod 600 /etc/letsencrypt/feno-rfc2136.ini
sudo certbot certonly \
--dns-rfc2136 \
--dns-rfc2136-credentials /etc/letsencrypt/feno-rfc2136.ini \
--dns-rfc2136-propagation-seconds 60 \
-d kunde.no -d '*.kunde.no'Nothing to pip install. The only reasons to prefer dns-feno are a firewall that blocks
outbound port 53, or wanting the credential to be a FENO API key rather than a TSIG key.
| Symptom | Meaning | Fix |
|---|---|---|
FENO rejected the API key (HTTP 401 INVALID_API_KEY) |
No key matches the token | Check for a truncated paste or a stray newline in feno.ini. |
FENO rejected the API key (HTTP 403 IP_NOT_ALLOWED) |
The host's IP is not on the key's allow-list | Add it, or create a key without an allow-list. |
Could not find a FENO domain covering … |
The key cannot see any zone that contains the name | The domain must be registered with FENO, on ns1.feno.no / ns2.feno.no, and visible to the key's account. |
… 400 NOT_FENO_NS |
The domain is delegated elsewhere | Move it to FENO nameservers, or use an acme-dns delegation through FENO. |
… 403 ACME_SCOPE_VIOLATION |
The acme:write key was aimed outside _acme-challenge* TXT |
Should not happen with this plugin; the message names the record it tried. |
FENO holds … but it does not use FENO nameservers |
The domain is on your account but delegated elsewhere | Same fix as NOT_FENO_NS. |
… SOA lookup used to find the zone … failed |
The key holds neither acme:write nor domains:read, and this host cannot resolve DNS |
Use an acme:write key (recommended), or fix resolution on the host. |
| The TXT record exists but the CA still fails | Propagation | Confirm with dig TXT _acme-challenge.kunde.no @ns1.feno.no +short, then raise --dns-feno-propagation-seconds. |
Run certbot with -v to see every API request and response the plugin makes (the API key is
never logged).
git clone https://github.com/MrErikCodes/certbot-dns-feno.git
cd certbot-dns-feno
python -m venv .venv && . .venv/bin/activate
pip install -e ".[test]"
pytest
certbot plugins # dns-feno should be listedTests mock the FENO API with requests-mock; nothing talks to the network.
MIT — see LICENSE. Copyright 2026 Erik Nilsen / FENO.