Skip to content

Repository files navigation

certbot-dns-feno

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-rfc2136 out of the box and it works against FENO's TSIG-authenticated DNS UPDATE gateway at ddns.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-rfc2136 instead below. Use this plugin when port 53 is blocked, or when you prefer an acme:write API 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.

Named arguments

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)

Installation

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).

Credentials

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/v1
sudo chmod 600 /etc/letsencrypt/feno.ini

The 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.

Examples

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.no

Wildcard certificate

A 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.

Renewal

Certbot reuses the plugin and the credentials file automatically:

sudo certbot renew --dry-run

How it works

  1. certbot hands the plugin the domain being validated (for *.kunde.no that is kunde.no), the challenge name (_acme-challenge.kunde.no) and the token.
  2. The plugin finds the zone FENO hosts by peeling labels off the domain one at a time and asking GET /v1/domains/{candidate} — the first 200 is the zone. .no holds both plain second-level names and delegated sub-zones, so the zone is never guessed from a label count. 404 DOMAIN_NOT_MANAGED moves 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. An acme:write key is allowed to make this call — it gets a minimal {domain, status, fenoNameservers} answer — and if fenoNameservers is false the plugin stops there with a message saying the domain is delegated elsewhere.
    • A key holding neither acme:write nor domains:read (for example dns:write alone) 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.
  3. It POSTs a TXT record to /v1/domains/{zone}/dns with the name relative to the zone (_acme-challenge, or _acme-challenge.www), TTL 60, and remembers the record id.
  4. 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.
  5. 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 — an acme:write key 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.

Using certbot's built-in --dns-rfc2136 instead

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.

Troubleshooting

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).

Development

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 listed

Tests mock the FENO API with requests-mock; nothing talks to the network.

License

MIT — see LICENSE. Copyright 2026 Erik Nilsen / FENO.

About

certbot DNS-01 authenticator plugin for FENO (feno.no)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages