Actions
40 actions, callable from the panel, the command palette and the API as
POST /api/v1/a/<id>. Internal actions used between modules are not listed.
| Action | What it does | Risk | Preview |
|---|---|---|---|
dns.server.install | Install the DNS server | high | No dry run |
dns.server.status | DNS server status (read) | low | No dry run |
dns.config.status | Hand-edited DNS settings (read) | low | No dry run |
dns.config.adopt | Keep a hand edit on top of the panel settings | medium | |
dns.config.reapply | Render the panel settings again | medium | |
dns.config.reset | Reset the settings to defaults | medium | |
dns.config.unmanage | Hand the settings file off | high | |
dns.zone.reconcile | Re-apply zones to the server | medium | No dry run |
dns.zone.create | Create a zone | low | |
dns.zone.list | List zones (read) | low | No dry run |
dns.zone.get | Get a zone (read) | low | No dry run |
dns.zone.delete | Delete a zone | high | |
dns.zone.suspend | Suspend a zone | medium | |
dns.zone.unsuspend | Resume a zone | low | |
dns.zone.import | Import a BIND zone file | medium | |
dns.zone.export | Export a BIND zone file (read) | low | No dry run |
dns.record.list | List records (read) | low | No dry run |
dns.record.create | Add a record | low | |
dns.record.update | Change a record | low | |
dns.record.delete | Delete a record | medium | |
dns.record.bulk | Change many records at once | medium | |
dns.record.set | Set the records of a name and type | low | |
dns.record.unset | Remove the records of a name and type | low | |
dns.history.list | Zone change history (read) | low | No dry run |
dns.history.restore | Restore an earlier zone state | medium | |
dns.template.list | List zone templates (read) | low | No dry run |
dns.template.save | Create or change a zone template | low | No dry run |
dns.template.delete | Delete a zone template | low | No dry run |
dns.template.apply | Apply a template to a zone | medium | |
dns.dnssec.enable | Enable DNSSEC | medium | |
dns.dnssec.disable | Disable DNSSEC | high | |
dns.dnssec.ds | Get the DS record (read) | low | No dry run |
dns.nameservers.get | Get the nameservers for new zones (read) | low | No dry run |
dns.nameservers.set | Set the nameservers for new zones | medium | No dry run |
dns.config.reset_all | Reset the whole service to defaults | high | |
dns.config.snapshot.list | Snapshots before resets (read) | low | No dry run |
dns.config.snapshot.restore | Undo a reset (restore a snapshot) | high | |
dns.config.history.list | Earlier versions of a file (read) | low | No dry run |
dns.config.history.diff | Compare an earlier version (read) | low | No dry run |
dns.config.history.restore | Restore an earlier version of a file | high |
Permissions and limits
Permissions
dns.zone.viewView zones and recordsdns.zone.manageCreate, suspend and delete zonesdns.record.manageEdit records and import zone filesdns.template.manageManage zone templatesdns.dnssec.manageManage DNSSECdns.nameserver.manageSet nameservers for new zonesdns.server.manageInstall and reconcile the DNS server
Plan limits
dns.zonesDNS zonesdns.records_per_zoneRecords per zone
Engineering notes
Generated from modules/dns/docs.md at build d90e9e2. These are the notes the engineers keep
next to the code: precise, technical, and honest about what is not done yet.
Desired state (zones, records, templates, history, nameserver sets) lives in schema m_dns; PowerDNS on the DNS node
is only ever told the complete zone (dns.zone.apply, agent op). Apply == reconcile: the agent diffs against the
server and PATCHes the changed record sets in one API call; the SOA serial (YYYYMMDDnn) moves only when something
changed. Refusal by the node undoes the change; an unreachable node keeps it and marks the zone error until
dns.zone.reconcile (accounts pattern).
Backend choice
Default pgsql: own Postgres database pdns + role pdns created on the node by the agent (postgres local
cluster, password generated on the node, kept only in /etc/powerdns/pdns.d/rc.conf; never the panel DB, ADR 0012 §5).
sqlite3 (backend: sqlite3) for the Micro profile. The pdns API listens on 127.0.0.1:8081 only; its key is
generated by the control plane, stored in the vault (pdns_api_key:<node>) and written to the node config.
pdns answers on every non-loopback address of the node (plus 127.0.0.1) so systemd-resolved's stub on 127.0.0.53 is
not disturbed. Packages are installed under a policy-rc.d hold so pdns never starts unconfigured.
Contracts other modules may rely on (stable)
Callers pass zone as a name (example.com) or id. Names are relative (www, @) or absolute with a trailing dot.
All are idempotent and return {zone, changed, applied, changes, warning?}.
dns.record.set {zone, name, type, ttl?, values[], replace_prefix?}- replace the record set, or only records of that name+type whose content starts withreplace_prefix(mail: SPFv=spf1, DKIM TXT atdefault._domainkey, DMARC, MX with10 mail.example.com.; MX content needs the priority).dns.record.unset {zone, name, type, value?, prefix?}- removes; nothing to remove is success.dns.record.create|update|delete|bulkby record id for editors;dns.record.list,dns.zone.get.dns.acme_challenge.set {domain, value}/dns.acme_challenge.clear {domain, value?}- internal: callable only as thesystemactor (ssl runs DNS-01 from its own internal action/scheduler, not as the end user). Adds/removes a TXT at_acme-challenge.<name>(TTL 60) in the longest-suffix zone; several values coexist (wildcard + apex);*.is stripped. The call returns after the node answers with the value.- Events
dns.zone.created,dns.zone.deleted,dns.record.changed,dns.dnssec.enabled. - Subscriptions:
domains.created-> zone from templatedefault(account's, else global, else builtin: apex A/AAAA, www CNAME, mail A/AAAA, MX 10 mail.., SPF v=spf1 a mx ~allas placeholders for mail); subdomains add A/AAAA to the parent zone;domains.deleted-> zone removed (limit released);accounts.deleted-> every zone of the account removed.
Rules
Types A AAAA CNAME MX TXT SRV CAA NS PTR TLSA SSHFP DS HTTPS SVCB NAPTR URI LOC (SOA is zone-level). Content is
validated and canonicalised with miekg/dns; errors are field-level (content: ...). CNAME at apex, CNAME beside
other data, duplicates, out-of-zone absolute names, TTL differences inside one set (adding a record re-aligns its set),
no apex NS, TTL < min_ttl (setting, default 30) and > max_records_per_zone (setting, default 5000) are rejected.
TXT longer than 255 bytes is chunked. IDN names are punycode. Targets: @ = zone, a single label is relative, a
dotted name without trailing dot is taken as fully qualified.
Ownership: a name inside another account's zone is refused (admin override_ownership). Limit dns.zones is
reserved per account. dns.records_per_zone is declared but not enforceable per zone with the host's counter API;
the cap above is used instead (follow-up for the CTO: a host "read limit value" call).
Settings read from core.settings scope module:dns: node_id, ip4, ip6, nameservers, nameserver_ips,
hostmaster, default_ttl, min_ttl, max_records_per_zone, max_zonefile_bytes.
Nameservers for new zones: account set -> server default set (dns.nameservers.set) -> settings -> ns1/ns2.<zone> with glue.
Every mutating action except the template/nameserver ones supports ?dry_run=1 and returns a Plan with
per-record diffs. History keeps 100 snapshots per zone; dns.history.restore writes a new entry.
DNSSEC: dns.dnssec.enable (one CSK, ECDSAP256SHA256) returns DS; reconcile re-enables it on a rebuilt node.
Verified in the lab (2026-10-09, instance w2-01, Ubuntu 24.04, pdns 4.8.3 + Postgres 16 on cp1)
dns.server.install (20 s, idempotent re-run); zone from domains.created event answered by dig @cp1 (SOA/NS/MX/TXT/
www CNAME/glue); same answers through the lab CoreDNS resolver; create/update/delete/set/unset/bulk (atomic), record
validation errors, import (bad lines reported, rest imported)/export round trip (re-import = no change), history +
restore, template save/apply, vanity nameservers + glue, wildcard, IDN (Bücher.lab.test -> xn--bcher-kva), DNSSEC
enable (RRSIG served, DS returned) and disable, suspend (REFUSED) / resume, ownership conflicts, dry-run plans, agent
outage (saved, zone error, dns.zone.reconcile fixes), drift (zone deleted behind our back, reconcile restores),
pdns restart persistence, account deletion removing zones.
Not exercisable through the REST API (internal, system actor only): dns.acme_challenge.* and the event actions'
failure branches; the ACME path shares mutate with dns.record.set (tested). sqlite3 backend: schema/config
rendering only, not run in the lab. Pdns caches negative answers for 5 s (negquery-cache-ttl=5).
The lab CoreDNS swap to Corefile.pdns happens in lab/up.sh only when pdns is already active at lab-up; in this
run it was swapped by hand after dns.server.install (sed 's/10.77.0./10.77.N./g' lab/fakenet/Corefile.pdns > lab/.run/<id>/dns/Corefile).
Not done / next
UI; secondaries + TSIG (cluster card); DDNS tokens; external providers (Cloudflare/Hostinger) and registrar DS push; health/propagation checks; applying a changed nameserver set to existing zones; ALIAS type.
External zones (w7-03)
When a domain's DNS lives at a provider (providers module link mode external): dns.on_domain_created creates no node zone;
dns.record.set|unset|list for such a name (no local zone) delegate to providers.dns.record.* as the same actor; dns.acme_challenge.set|clear
delegate to providers.acme_challenge.* when no local zone holds the name. dns has no depends_on for providers (providers depends on dns); if
providers is absent the calls fail soft (zone created / not found as before). Dry runs of an external set/unset return a one-line plan (SDK Host.Call carries no dry-run flag).
dns.zone.import in replace mode now removes what the file drops in a first step and adds in a second (PowerDNS refuses a CNAME<->A swap in one patch).