Orchestrating Network Autonomy: A Deep Dive into Kea DHCP and Bind9 DDNS Integration
In a sophisticated DevOps laboratory, manual IP management is a relic of the past. To achieve true infrastructure-as-code and seamless service discovery, we must implement a system where the network layer reacts dynamically to the presence of new virtual machines. This article details the implementation of a high-performance DHCP/DNS stack using Kea DHCP and Bind9.
1. Architectural Strategy: The Three-Tier Network Split
To prevent IP address exhaustion and cross-talk between different environments (Home, Lab, and Kubernetes), we partitioned the 172.21.5.0/24 subnet into three distinct functional zones:
- Consumer Zone (TP-Link Router): Handles conventional devices (PCs, smartphones).
- Range:
.10to.90
- Range:
- Lab Infrastructure (Kea DHCP): Our focus. Manages Vagrant VMs, Jenkins, GitLab, and K8s nodes.
- Range:
.100to.200
- Range:
- Kubernetes Service Layer (MetalLB): Reserved for LoadBalancer services within the cluster.
- Range:
.240to.250
- Range:
This separation ensures that a rogue VM won’t hijack an IP intended for a critical Kubernetes service.
2. The Foundation: Securing Updates with TSIG
Dynamic DNS updates must be authenticated to prevent unauthorized record injection. We utilize TSIG (Transaction Signature) via the rndc-key.
Extracting the Key
The key is generated by Bind9 and resides in /etc/bind/rndc.key. This shared secret is the “handshake” between the DHCP server and the DNS server.
Bash
# cat /etc/bind/rndc.key
key "rndc-key" {
algorithm hmac-sha256;
secret "U0Rlzk+coc4K6wtMxJ2T6IsSSyfl+x9sca5o64css6c=";
};
3. Configuring Kea DHCP: The Modern Successor to ISC-DHCP
Kea is modular. We utilize the Dhcp4 engine for IP allocation and the DhcpDdns engine to communicate with Bind9.
DHCP4 Configuration (/etc/kea/kea-dhcp4.conf)
The core configuration defines the interface, the pool, and the DDNS trigger.
JSON
{
"Dhcp4": {
"interfaces-config": {
"interfaces": [ "eth1" ],
"dhcp-socket-type": "raw"
},
"control-socket": {
"socket-type": "unix",
"socket-name": "/tmp/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"lfc-interval": 3600,
"name": "/var/lib/kea/kea-leases4.csv"
},
"expired-leases-processing": {
"reclaim-timer-wait-time": 10,
"flush-reclaimed-timer-wait-time": 25,
"hold-reclaimed-time": 3600,
"max-reclaim-leases": 100,
"max-reclaim-time": 250,
"unwarned-reclaim-cycles": 5
},
"renew-timer": 900,
"rebind-timer": 1800,
"valid-lifetime": 3600,
"option-data": [
{
"name": "domain-name-servers",
"data": "172.21.5.155"
},
{
"name": "domain-name",
"data": "devops-db.local"
},
{
"name": "domain-search",
"data": "devops-db.local, devops-db.internal"
}
],
"dhcp-ddns": {
"enable-updates": true,
"server-ip": "127.0.0.1",
"server-port": 53001,
"ncr-protocol": "UDP",
"ncr-format": "JSON",
"override-no-update": true,
"override-client-update": true,
"replace-client-name": "when-not-present",
"generated-prefix": "host",
"qualifying-suffix": "devops-db.local"
},
"subnet4": [
{
"id": 1,
"subnet": "172.21.5.0/24",
"pools": [
{
"pool": "172.21.5.100 - 172.21.5.200"
}
],
"option-data": [
{
"name": "routers",
"data": "172.21.5.1"
}
],
"reservations": []
}
],
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "/var/log/kea/kea-dhcp4.log"
}
],
"severity": "INFO",
"debuglevel": 0
}
]
}
}DDNS Bridge Configuration (/etc/kea/kea-dhcp-ddns.conf)
This service acts as a middleman, receiving requests from DHCP and signing them with the TSIG key for Bind.
JSON
{
"DhcpDdns": {
"ip-address": "127.0.0.1",
"port": 53001,
"control-socket": {
"socket-type": "unix",
"socket-name": "/tmp/kea-ddns-ctrl-socket"
},
"tsig-keys": [
{
"name": "rndc-key",
"algorithm": "hmac-sha256",
"secret": "U0Rlzk+coc4K6wtMxJ2T6IsSSyfl+x9sca5o64css6c="
}
],
"forward-ddns": {
"ddns-domains": [
{
"name": "devops-db.local.",
"key-name": "rndc-key",
"dns-servers": [ { "ip-address": "127.0.0.1", "port": 53 } ]
},
{
"name": "devops-db.internal.",
"key-name": "rndc-key",
"dns-servers": [ { "ip-address": "127.0.0.1", "port": 53 } ]
}
]
},
"reverse-ddns": {
"ddns-domains": [
{
"name": "5.21.172.in-addr.arpa.",
"key-name": "rndc-key",
"dns-servers": [ { "ip-address": "127.0.0.1", "port": 53 } ]
}
]
},
"loggers": [
{
"name": "kea-dhcp-ddns",
"output_options": [ { "output": "/var/log/kea/kea-ddns.log" } ],
"severity": "INFO"
}
]
}
}4. Bind9: Preparing the Zone for Dynamic Updates
The DNS server must be told to trust the rndc-key and allow it to modify specific zones.
The Master Config (/etc/bind/named.conf)
Crucial: The include "/etc/bind/rndc.key"; must appear before the zone definitions that reference it.
Plaintext
acl internal {
127.0.0.0/8;
172.21.5.0/24;
172.25.1.0/24;
};
options {
forwarders {
1.1.1.1;
8.8.8.8;
};
allow-query { internal; };
directory "/var/cache/bind";
};
key "devops-key" {
algorithm hmac-sha256;
secret "vfwR+fr9ITEdLsnJYjAZmqX+dhcJQUXZQWX3TmLcxEk=";
};
include "/etc/bind/rndc.key";
zone "devops-db.local" {
type master;
file "/var/lib/bind/devops-db.local";
allow-query { any; };
allow-update { key "rndc-key"; };
};
zone "devops-db.internal" {
type master;
file "/var/lib/bind/devops-db.internal";
allow-update { key "devops-key"; key "rndc-key"; };
};
zone "5.21.172.in-addr.arpa" {
type master;
file "/var/lib/bind/db.172.21.5";
allow-query { any; };
allow-update { key "rndc-key"; };
};
zone "devops-db.info" IN {
type master;
file "/etc/bind/devops-db.info";
};
controls {
inet 127.0.0.1 allow { localhost; } keys { "rndc-key"; };
};
logging {
channel information {
file "/var/log/named/bind.log" versions 3 size 500K;
severity debug 10;
print-time yes;
print-severity yes;
print-category yes;
};
channel query {
file "/var/log/named/bind-query.log" versions 5 size 10M;
severity debug 10;
print-time yes;
print-severity yes;
print-category yes;
};
category default {information;};
category update { information; };
category update-security { information; };
category security { information; };
};Permission and Write Access
Dynamic updates create .jnl (journal) files. On Ubuntu, Bind (AppArmor and Linux permissions) is restricted to writing in /var/lib/bind/. Zones placed in /etc/bind/ will fail to update.
Bash
sudo chown -R bind:bind /var/lib/bind
sudo chmod -R 775 /var/lib/bind
5. Validation: The “Moment of Truth”
After restarting the services (named, kea-dhcp4-server, kea-dhcp-ddns-server), we force a client to request a new lease.
Client-Side Execution
Bash
sudo dhclient -v -r eth1 && sudo dhclient -v eth1
- What to expect: The client kills the old process, sends a
DHCPDISCOVER, receives anDHCPOFFERof.100from.155, and finally binds to the address.
Server-Side DDNS Logs
Monitoring /var/log/kea/kea-ddns.log should reveal:
DHCP_DDNS_REMOVE_SUCCEEDED: Cleaning up old records.DHCP_DDNS_ADD_SUCCEEDED: Successfully injecting theArecord for the client FQDN.
Verification with dig
Finally, a cross-node check (e.g., from GitLab to Jenkins):
Bash
dig srv-infrastructure-jenkins-master-01.devops-db.local +short
# Result: 172.21.5.100
Conclusion: Automating the Future
This setup creates a hands-off environment. Whether a VM is booting for the first time, renewing its lease at the 50% mark, or being replaced by a new node, the DNS layer remains accurate. We have effectively decoupled our infrastructure logic from static IP dependencies, enabling true DevOps scalability.
