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:

  1. Consumer Zone (TP-Link Router): Handles conventional devices (PCs, smartphones).
    • Range: .10 to .90
  2. Lab Infrastructure (Kea DHCP): Our focus. Manages Vagrant VMs, Jenkins, GitLab, and K8s nodes.
    • Range: .100 to .200
  3. Kubernetes Service Layer (MetalLB): Reserved for LoadBalancer services within the cluster.
    • Range: .240 to .250

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 an DHCPOFFER of .100 from .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 the A record 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.