LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Kea DHCP: The Successor to ISC DHCP

dhcpkeaiscnetworkinghomelablinux

ISC DHCP (dhcpd) served the internet for 25+ years. It shipped on every mainstream Linux distro, ran in front of almost every corporate LAN, and was the definitive reference implementation of DHCP. On October 5, 2022, ISC officially ended support. The successor — from the same organization — is Kea, a ground-up rewrite with a JSON configuration model, hooks for dynamic behavior, a REST API, and HA baked in.

If you’re still running dhcpd.conf, this post is your migration guide. If you’re starting fresh, it’s your explanation of why Kea is the current answer. Kea isn’t a drop-in replacement — the config syntax is completely different — but the conceptual model is similar, and the features are an upgrade on every dimension.

Why Kea exists

ISC DHCP was written in the 1990s, and it showed. The config language was idiosyncratic (its own grammar with statements, classes, groups), the code base was dense, and key modern features — REST API for automation, HA that didn’t require external tools, programmatic hooks — required extensive workarounds.

Kea, started in 2014 and declared production-ready around 2018, was architected for modern deployments:

  • JSON config you can generate from Ansible, Terraform, CMDBs.
  • Hooks libraries (shared libs) to customize lease allocation, logging, and class matching.
  • REST API via the Control Agent for runtime management.
  • Built-in HA using a proper failover protocol (not ISC DHCP’s awkward “failover peer”).
  • Backend flexibility: leases in memory, files, MySQL, PostgreSQL, Cassandra.
  • Separate DHCPv4 and DHCPv6 daemons so you can run one without the other.
  • DDNS daemon (kea-dhcp-ddns) for dynamic DNS updates.

Architecture

A Kea deployment consists of several processes:

  • kea-dhcp4 — the DHCPv4 server.
  • kea-dhcp6 — the DHCPv6 server.
  • kea-dhcp-ddns (d2) — forwards DDNS updates to your DNS server.
  • kea-ctrl-agent — optional REST API front-end.

Each has its own JSON config file. In practice most homelabs and SMB deployments run only kea-dhcp4, often with kea-ctrl-agent for automation.

The lease backend is configurable. Default is a memfile (leases4.csv), which is simple and works for single-server deployments. For HA or fleet-wide visibility, use MySQL or PostgreSQL.

Installing Kea

Every major distro packages it:

1
2
3
4
5
6
7
8
# Debian/Ubuntu
apt install kea-dhcp4-server kea-ctrl-agent kea-admin

# Fedora/RHEL (EPEL for older versions; native in newer)
dnf install kea

# Alpine
apk add kea-dhcp4

For the newest features, use the ISC-maintained repositories at cloudsmith.io/isc.

A minimal DHCPv4 config

Kea configs are JSON. Comments are tolerated (despite JSON spec) to make them human-editable.

/etc/kea/kea-dhcp4.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{
    "Dhcp4": {
        "interfaces-config": {
            "interfaces": [ "eth0" ]
        },
        "lease-database": {
            "type": "memfile",
            "persist": true,
            "name": "/var/lib/kea/dhcp4.leases"
        },
        "valid-lifetime": 4000,
        "renew-timer": 1000,
        "rebind-timer": 2000,

        "option-data": [
            {
                "name": "domain-name-servers",
                "data": "10.0.0.1, 1.1.1.1"
            },
            {
                "name": "domain-search",
                "data": "lab.example.com"
            }
        ],

        "subnet4": [
            {
                "id": 1,
                "subnet": "10.0.0.0/24",
                "pools": [
                    { "pool": "10.0.0.100 - 10.0.0.200" }
                ],
                "option-data": [
                    { "name": "routers", "data": "10.0.0.1" }
                ],
                "reservations": [
                    {
                        "hw-address": "aa:bb:cc:dd:ee:01",
                        "ip-address": "10.0.0.50",
                        "hostname": "printer"
                    }
                ]
            }
        ],

        "loggers": [
            {
                "name": "kea-dhcp4",
                "severity": "INFO",
                "output-options": [
                    { "output": "/var/log/kea/dhcp4.log" }
                ]
            }
        ]
    }
}

Start and enable:

1
systemctl enable --now kea-dhcp4-server

That’s a working DHCPv4 server serving 10.0.0.100-200 to clients on eth0, with DNS and a static reservation for a printer.

Pool vs reservation

  • Pool: a range of dynamic addresses. Allocated first-come, first-served, leases expire and return to the pool.
  • Reservation: a fixed binding of hardware address (or other identifier) to an IP. Takes priority over pool allocation.

Reservations can be stored in the config (as above), in the lease database, or in a separate host database (hosts-database stanza, usually MySQL/Postgres for fleet use).

Subnet IDs

Every subnet must have a unique id. Kea uses it as a stable reference for lease accounting and API operations. Don’t reuse IDs when deleting/recreating subnets — stale lease entries can bind to wrong subnets.

Client classification

Kea’s client classes are one of its strongest features. You can match clients by vendor, MAC prefix, hostname, custom option, and then serve them different configs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
"client-classes": [
    {
        "name": "pxe-boot",
        "test": "option[77].hex == 0x4950584500",
        "boot-file-name": "ipxe.efi",
        "next-server": "10.0.0.10"
    },
    {
        "name": "iot-vlan",
        "test": "substring(hexstring(pkt4.mac,':'),0,8) == 'b8:27:eb'",
        "option-data": [
            { "name": "domain-name-servers", "data": "10.0.0.2" }
        ]
    }
]

Classes with only-in-additional-list: true apply only when referenced; others apply to every matching client automatically. Combine with per-subnet pools assigned to specific classes for tiered address allocation:

1
2
3
4
"pools": [
    { "pool": "10.0.0.100 - 10.0.0.150", "client-classes": [ "trusted" ] },
    { "pool": "10.0.0.151 - 10.0.0.200" }
]

This is the mechanism for things like:

  • Serving PXE boot information only to UEFI clients.
  • Assigning Raspberry Pis (OUI b8:27:eb) to a specific DNS.
  • Steering known VoIP phones to a TFTP config server.
  • Rejecting unknown clients outright (set client-classes on all pools; non-matching clients get no lease).

High Availability

Kea’s HA is production-grade. Two modes:

Hot-standby

One primary serves all traffic; the standby takes over on failure. Simple, safe.

Load-balancing

Both servers serve different fractions of the address space based on a hash of the client identifier. True active-active.

A minimal HA config uses the ha hook library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"hooks-libraries": [
    {
        "library": "/usr/lib/kea/hooks/libdhcp_lease_cmds.so"
    },
    {
        "library": "/usr/lib/kea/hooks/libdhcp_ha.so",
        "parameters": {
            "high-availability": [{
                "this-server-name": "server1",
                "mode": "load-balancing",
                "heartbeat-delay": 10000,
                "max-response-delay": 60000,
                "max-unacked-clients": 5,
                "peers": [
                    {
                        "name": "server1",
                        "url": "http://10.0.0.1:8000/",
                        "role": "primary"
                    },
                    {
                        "name": "server2",
                        "url": "http://10.0.0.2:8000/",
                        "role": "secondary"
                    }
                ]
            }]
        }
    }
]

The HA hook needs lease_cmds loaded too; it uses the Control Agent on each peer to replicate lease state. Both peers need the Control Agent running on the URL specified.

HA with a MySQL or PostgreSQL lease backend simplifies things further — both peers read/write the same database, so replication is at the DB layer, not per-lease RPCs.

The Control Agent and REST API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// /etc/kea/kea-ctrl-agent.conf
{
    "Control-agent": {
        "http-host": "127.0.0.1",
        "http-port": 8000,
        "authentication": {
            "type": "basic",
            "clients": [
                { "user": "admin", "password": "hunter2" }
            ]
        },
        "control-sockets": {
            "dhcp4": {
                "socket-type": "unix",
                "socket-name": "/tmp/kea4-ctrl-socket"
            }
        }
    }
}

Once running, curl-able:

1
2
3
curl -u admin:hunter2 -X POST -H "Content-Type: application/json" \
    -d '{ "command": "lease4-get-all", "service": [ "dhcp4" ] }' \
    http://127.0.0.1:8000/

Commands include config-get, config-reload, lease4-add, lease4-del, ha-heartbeat, statistic-get-all, and many more. The API is the preferred way to automate Kea — don’t edit config files and reload when you can POST a lease directly.

Bind the Control Agent only to localhost or behind a reverse proxy with TLS. The basic auth is fine for internal use; for production expose it over TLS with mutual cert auth.

Dynamic DNS

kea-dhcp-ddns (d2) receives DNS update requests from kea-dhcp4 and forwards them to your DNS server. Useful for populating dhcp-hosts.lab.example.com. automatically.

Configure the DHCPv4 side:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"dhcp-ddns": {
    "enable-updates": true,
    "server-ip": "127.0.0.1",
    "server-port": 53001
},
"ddns-send-updates": true,
"ddns-override-client-update": false,
"ddns-override-no-update": false,
"ddns-replace-client-name": "never",
"ddns-qualifying-suffix": "lab.example.com.",

And the d2 side, with the DNS server it updates:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
    "DhcpDdns": {
        "ip-address": "127.0.0.1",
        "port": 53001,
        "forward-ddns": {
            "ddns-domains": [
                {
                    "name": "lab.example.com.",
                    "key-name": "d2-key",
                    "dns-servers": [ { "ip-address": "10.0.0.2" } ]
                }
            ]
        },
        "reverse-ddns": {
            "ddns-domains": [
                {
                    "name": "0.0.10.in-addr.arpa.",
                    "key-name": "d2-key",
                    "dns-servers": [ { "ip-address": "10.0.0.2" } ]
                }
            ]
        },
        "tsig-keys": [
            {
                "name": "d2-key",
                "algorithm": "HMAC-SHA256",
                "secret": "base64-key"
            }
        ]
    }
}

The DNS server (BIND/Knot) needs the matching TSIG key and permission to accept updates for those zones. Standard pattern; same key on both sides.

DHCPv6

kea-dhcp6 is architecturally similar but uses different option names and different pool syntax. A minimal v6 config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
    "Dhcp6": {
        "interfaces-config": {
            "interfaces": [ "eth0" ]
        },
        "lease-database": {
            "type": "memfile",
            "persist": true,
            "name": "/var/lib/kea/dhcp6.leases"
        },
        "preferred-lifetime": 3000,
        "valid-lifetime": 4000,
        "subnet6": [
            {
                "id": 1,
                "subnet": "2001:db8:1::/64",
                "pools": [
                    { "pool": "2001:db8:1::100 - 2001:db8:1::1ff" }
                ],
                "pd-pools": [
                    {
                        "prefix": "2001:db8:1000::",
                        "prefix-len": 48,
                        "delegated-len": 60
                    }
                ]
            }
        ]
    }
}

pd-pools are for prefix delegation — handing out whole /60 or /56 prefixes to routers that want to sub-delegate to downstream networks. Common in ISP deployments; rare in homelabs unless you’re experimenting with IPv6 routing across internal subnets.

Most networks run DHCPv6 alongside SLAAC. Typical pattern: router advertisements with the M flag set for stateful configuration, Kea DHCPv6 providing addresses and DNS.

Lease backends and storage

Four options:

  1. memfile (default) — in-memory with periodic flush to CSV. Simple, fast, single-host only.
  2. MySQLkea-dhcp4-server with kea-admin lease-init mysql to set up schema. Supports HA and fleet queries.
  3. PostgreSQL — same, with kea-admin lease-init pgsql.
  4. Cassandra — for large-scale eventually-consistent deployments.

For a multi-server HA setup, MySQL or PostgreSQL is the natural choice. Configure with:

1
2
3
4
5
6
7
8
"lease-database": {
    "type": "postgresql",
    "host": "10.0.0.20",
    "port": 5432,
    "user": "kea",
    "password": "secret",
    "name": "kea"
}

Ensure the DB is HA (Patroni, RDS, etc.). If the DB goes down, Kea can’t lease.

Observability

Kea exposes statistics via the Control Agent:

1
2
3
curl -u admin:pw -X POST -H "Content-Type: application/json" \
    -d '{ "command": "statistic-get-all", "service": [ "dhcp4" ] }' \
    http://127.0.0.1:8000/

Output includes per-subnet counters: assigned-addresses, declined-addresses, reclaimed-leases, pkt4-received, pkt4-sent, and finer breakdowns by packet type.

Feed this into Prometheus with kea-exporter (community Python exporter). Point Grafana at it and alert on:

  • Pool exhaustion (assigned-addresses / total-addresses > 0.85).
  • Decline rate spike — clients rejecting leases often means duplicate IPs on the network.
  • Packet drops / unanswered queries.
  • HA state change to partner-down or terminated.

Migration from ISC DHCP

The bad news: the config language is entirely different. There’s no direct converter that works on non-trivial configs.

The good news: the concepts map 1:1. Subnets, pools, host declarations, classes — all exist in Kea with similar semantics.

A typical dhcpd.conf:

subnet 10.0.0.0 netmask 255.255.255.0 {
    range 10.0.0.100 10.0.0.200;
    option routers 10.0.0.1;
    option domain-name-servers 10.0.0.1, 1.1.1.1;
    default-lease-time 4000;
    max-lease-time 7200;
}

host printer {
    hardware ethernet aa:bb:cc:dd:ee:01;
    fixed-address 10.0.0.50;
}

The Kea equivalent was shown earlier in this post. Structurally identical; syntactically unrelated.

Practical migration path:

  1. Stand up Kea alongside ISC DHCP, not on the wire.
  2. Translate the config section by section.
  3. Validate with kea-dhcp4 -t /etc/kea/kea-dhcp4.conf (test mode).
  4. Test on a dev VLAN with a few clients.
  5. Cutover: stop ISC DHCP, start Kea. Have a rollback plan.

For large fleets with hundreds of reservations, write a script to parse the ISC config and emit Kea JSON. There are a few community converters; none are perfect, but they handle 80% of typical configs.

Common pitfalls

  1. Forgetting persist: true on memfile. Server crashes, all leases lost, every client appears new, pool exhaustion.
  2. Running Kea on multiple interfaces without specifying them. Default is listen on no interfaces; you must list them explicitly.
  3. HA peers with mismatched configs. Both peers must have identical subnet and pool definitions. Divergence causes lease corruption.
  4. Wrong MTU/firewall for Control Agent. Binding publicly without auth. Use TLS + mutual auth if the CA is exposed at all.
  5. Running Kea and dnsmasq on the same interface. Both answer DHCP; clients get random results. Pick one.
  6. Large reservations inside the JSON config. Fine for dozens; use host databases for hundreds/thousands.
  7. Not monitoring pool utilization. Pool exhaustion is sudden and silent until someone can’t get an IP.
  8. DDNS without TSIG. Updates work but anyone on the network can forge them. Always TSIG-sign DDNS.
  9. Skipping kea-admin db-upgrade after a version upgrade. Schema changes between versions; the DB may need migration.
  10. Authentication on the Control Agent set to basic auth over HTTP on a public interface. Don’t. Use HTTPS + basic auth, or localhost-only with TLS reverse proxy.

Small-deployment recipe

For a homelab with a single router:

  • One kea-dhcp4 process.
  • Memfile backend.
  • No HA, no Control Agent.
  • 3-4 subnets for VLANs with different pools and class-based DNS steering.
  • Static reservations in the config file.
  • Logs to /var/log/kea/ with rotation.

You’ve replaced dnsmasq or isc-dhcp-server with something that scales if you ever need it to.

Fleet deployment recipe

For real workloads:

  • Two Kea servers, load-balancing HA via libdhcp_ha.so.
  • PostgreSQL lease database (HA via Patroni).
  • Host reservations also in PostgreSQL (hosts-database).
  • Control Agent behind HAProxy with mTLS.
  • kea-exporter → Prometheus → Grafana with alerts on pool utilization and HA state.
  • Ansible-templated config, git-versioned.
  • DDNS to internal BIND with TSIG.

When to use something else

  • Very small deployment (1–2 VLANs, few hosts): dnsmasq is simpler. It’s DHCP + DNS + TFTP + PXE in one binary, all configured with about 10 lines. Kea’s power is overkill.
  • ISP-scale DHCPv6 with IPoE: purpose-built systems (FreeRADIUS + DHCP, vendor BNGs).
  • Cloud-native only: cloud providers handle DHCP; you don’t run a server.

Kea is the right answer when you want DHCP that feels like modern software: declarative config, API-driven, HA-capable, observable.

Looking forward

Kea’s feature set keeps expanding. Recent versions added:

  • Config backend (cb-cmds) — store config in MySQL/Postgres, managed via API. Great for large fleets.
  • Flex-id hook — use custom client identifiers (e.g., derived from Option 82 for ISP subscriber ID).
  • RADIUS hook — query RADIUS for authorization before leasing.
  • GSS-TSIG — Kerberos-authenticated DDNS for Windows environments.
  • Perfmon hook — detailed per-subnet performance monitoring.

It’s actively developed and will outlast whatever’s next.


If you’re still running dhcpd because “it works”, it probably still does. But the ground shifted in 2022: ISC DHCP is EOL, distros are removing it, security patches have stopped. Kea is the successor, written by the same organization, and once you’re past the JSON-vs-statement syntax difference, it does everything ISC DHCP did plus enough more that you’ll probably find a reason to like the change.

Comments