> ## Documentation Index
> Fetch the complete documentation index at: https://dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dynamic updates (RFC 2136)

> Let DHCP servers, ExternalDNS, certbot and nsupdate change records with standard DNS UPDATE messages signed with a TSIG key.

Programs can change records with standard **DNS UPDATE** messages (RFC 2136) signed with a
**TSIG** key (RFC 8945). This is what DHCP servers (Kea, ISC DHCP), Kubernetes **ExternalDNS**,
`certbot-dns-rfc2136`, lego and Traefik's `rfc2136` provider, and `nsupdate` speak. They send
updates to DNZ's DNS port, the same one clients query.

Keys belong to a **division** and live under **Settings → API tokens → DNS update keys**, next to
API tokens. Managing them needs the `tokens.manage` permission (Owner by default).

> [!NOTE]
>
> For tools that speak HTTP instead, use an API token with the ACME or DynDNS2 endpoints. See
> [ACME certificates](/integrations/acme) and [Dynamic DNS](/integrations/dynamic-dns).

## Create a key

**Dashboard**

Click **Create update key**. The **Kea DHCP**, **ExternalDNS** and **certbot / ACME** buttons
fill in a sensible scope. The form asks for:

| Field | Meaning |
| - | - |
| **Key name** | The TSIG name clients sign with, such as `kea-ddns` or `certbot.example.com`. Must be unique on the server, replication keys included. |
| **Algorithm** | `hmac-sha256` (default), `hmac-sha384` or `hmac-sha512`. |
| **Secret** | DNZ makes a random 256-bit one, or paste one a client already uses (base64, at least 16 bytes). |
| **Zones** | Every zone in the division, or only some. |
| **Names** | Full names the key may change. Empty means any name in its zones. |
| **Record types** | For example `TXT` only for ACME. Empty means any type. |
| **Allowed from** | Addresses or networks the updates may come from. Empty means anywhere. |
| **Allow zone transfers (AXFR)** | Lets the key transfer its zones as well. ExternalDNS needs this to read a zone before changing it. |
| **Expires** | Never, or after 30 days, 90 days or a year. |

In **Names**, `*` matches anything, dots included: `*.dhcp.example.com` covers every name below
`dhcp.example.com`, and `_acme-challenge.*` covers every ACME challenge.

**CLI**

```bash theme={null}
dnz update-key create kea-ddns -t A,AAAA,PTR,DHCID -m '*.dhcp.example.com' --allow-from 10.0.0.0/8 > kea.key
dnz update-key create external-dns -z example.com -t A,AAAA,CNAME,TXT --axfr
dnz update-key create acme -t TXT -m '_acme-challenge.*'
dnz update-key list
dnz update-key set 3 --type A,TXT --any-name --axfr off
dnz update-key set 2 --allow-from 10.0.0.0/8 --expires-in-days 365
dnz update-key rm 3
```

`dnz update-key create` prints a BIND-style key file on standard output, ready for
`nsupdate -k`. See [CLI: access](/cli/access).

The secret is shown **once**, together with ready-to-paste settings for nsupdate, Kea, ExternalDNS
and certbot. If it gets lost, revoke the key and create a new one.

**Edit scope** changes zones, names, types, networks, AXFR and expiry. The name and secret stay the
same.

## What a key may do

A key never has more rights than the person who created it. Every update is checked again:

* The key hasn't expired.
* Its creator is still active and still has `records.write` in the division. For AXFR they need
  `zones.read` too.
* The zone is a **primary** zone of the key's division and is switched on. Secondary zones are
  refused: update their primary instead.
* Every name and type in the update is inside the key's scope. Deleting everything at a name is
  allowed only when the key covers every type there.
* The update comes from an allowed address.

If any of these fails, the whole update is refused (`REFUSED`). Unsigned updates, or updates signed
with an unknown key or a wrong secret, are refused as well (`NOTAUTH` with a TSIG error). Clocks may
differ by up to 5 minutes.

## How updates are applied

DNZ follows RFC 2136:

1. **Prerequisites** are checked first: name in use / not in use, RRset exists / doesn't exist, and
   RRset exists with exactly these values. If one fails, nothing changes and the answer says which
   (`YXDOMAIN`, `NXDOMAIN`, `YXRRSET`, `NXRRSET`).
2. Then the updates run in order: add RRs, delete an RRset, delete everything at a name, or delete
   one RR.
3. The whole message is **one transaction**. The zone's serial goes up once, secondaries get a
   NOTIFY, and the change is written to the [audit log](/admin/audit-log) as *changed records by
   DNS UPDATE*, by `<creator> via <key>`. An update that changes nothing leaves the serial alone.

> **DNZ-specific details**
>
> * DNZ writes the zone's **SOA** itself, so SOA updates are ignored. The apex NS records and the
>   SOA survive "delete everything at the apex", as the RFC requires.
> * **CNAME** rules follow the RFC: a CNAME next to other data, or data next to a CNAME, is
>   silently ignored. A new CNAME target replaces the old one. The apex never gets a CNAME.
> * Adding a value that is already there only updates its TTL. All records of an RRset get the
>   TTL of the newest addition. TTLs are kept between 1 second and 7 days.
> * Only a zone's **default records** are touched, never records of a split-horizon
>   [view](/dns/views).
> * **Records that are turned off count as absent.** Prerequisites don't see them, adding the
>   same value turns the record back on, and deletes remove them.
> * DNSSEC records (RRSIG, NSEC, DNSKEY…) are made by DNZ and can't be updated. DS records can.
> * New records get the comment *Added by DNS UPDATE (key)*.

> **Types the editor doesn't know**
>
> Records of the editor's types (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, PTR, HTTPS, SVCB, TLSA,
> SSHFP, NAPTR, DS, URI, DNAME, LOC) are stored the normal way and stay editable in the UI.
> Anything else is stored **exactly as received** and served as is. That covers DHCID (Kea) and
> TXT records split into several strings (DKIM).
>
> These records show **Read-only** in the zone editor: they can be turned off, tagged or deleted
> there, but not edited. Types DNZ's DNS library has no name for are kept in RFC 3597's generic
> form (`\# 4 0a000001`).

## Clients

In the examples, DNZ answers DNS at `dns.example.com` (or `192.0.2.53`) on port 53. Use your
server's address and DNS port, which is shown under the key list.

**nsupdate**

```text dnz.key theme={null}
key "kea-ddns" {
  algorithm hmac-sha256;
  secret "base64-secret";
};
```

```bash theme={null}
nsupdate -k dnz.key <<'EOF'
server dns.example.com 53
zone example.com
prereq nxdomain pc1.example.com
update add pc1.example.com 300 A 192.0.2.10
send
EOF
```

**Kea DHCP-DDNS**

Give the key `A`, `AAAA`, `PTR` and `DHCID`, and cover the reverse zones too (for example
`2.0.192.in-addr.arpa` as a zone in DNZ). Then add this to `kea-dhcp-ddns.conf`:

```json kea-dhcp-ddns.conf theme={null}
{
  "DhcpDdns": {
    "tsig-keys": [{ "name": "kea-ddns", "algorithm": "HMAC-SHA256", "secret": "base64-secret" }],
    "forward-ddns": {
      "ddns-domains": [{ "name": "example.com.", "key-name": "kea-ddns", "dns-servers": [{ "ip-address": "192.0.2.53", "port": 53 }] }]
    },
    "reverse-ddns": {
      "ddns-domains": [{ "name": "2.0.192.in-addr.arpa.", "key-name": "kea-ddns", "dns-servers": [{ "ip-address": "192.0.2.53", "port": 53 }] }]
    }
  }
}
```

Kea checks DHCID records before changing a name (RFC 4703), so leave `DHCID` in the key's
types.

**ExternalDNS**

Give the key `A`, `AAAA`, `CNAME` and `TXT` (ExternalDNS keeps its ownership records in TXT),
and turn on **Allow zone transfers**. Then run ExternalDNS with:

```text theme={null}
--provider=rfc2136
--rfc2136-host=dns.example.com
--rfc2136-port=53
--rfc2136-zone=example.com
--rfc2136-tsig-keyname=external-dns
--rfc2136-tsig-secret=base64-secret
--rfc2136-tsig-secret-alg=hmac-sha256
--rfc2136-tsig-axfr
```

**certbot**

Use a key limited to `TXT` at `_acme-challenge.*`. With the `certbot-dns-rfc2136` plugin:

```ini rfc2136.ini theme={null}
# chmod 600
dns_rfc2136_server = 192.0.2.53
dns_rfc2136_port = 53
dns_rfc2136_name = acme
dns_rfc2136_secret = base64-secret
dns_rfc2136_algorithm = HMAC-SHA256
```

```bash theme={null}
certbot certonly --dns-rfc2136 --dns-rfc2136-credentials rfc2136.ini -d example.com -d '*.example.com'
```

certbot finds a name's zone with a signed SOA query. DNZ answers those for update keys as well.

**lego, Traefik**

lego and Traefik use the same settings through their `rfc2136` provider:

```bash theme={null}
RFC2136_NAMESERVER=192.0.2.53:53
RFC2136_TSIG_KEY=acme
RFC2136_TSIG_SECRET=base64-secret
RFC2136_TSIG_ALGORITHM=hmac-sha256.
```

## Troubleshooting

| Answer | Likely cause |
| - | - |
| `REFUSED` | The key doesn't cover the zone, name, type or address, the zone is a secondary, or the key expired or its creator lost access. DNZ logs the reason at `info` level (*dynamic update refused: …*). |
| `NOTAUTH` with `BADKEY` / `BADSIG` | The key name or secret doesn't match, or the clocks differ by more than 5 minutes. |
| `NOTAUTH` without a TSIG error | DNZ doesn't serve that zone, or the zone is turned off. |

**Last used** in the key list shows when and from where each key last made an update.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.