Kea, Ansible & FreeBSD
I've been testing ISC Kea DHCP at home for some time now, mostly due to
$REASONS. The only downside so far (aside from learning a tool that
is easily replaced by
dnsmasq
) has been it's inability to use subnetspecific domain suffixes, something supported by both the old ISC DHCP
implementation and tools like dnsmasq
.
Update 2023: The above is incorrect, isc-kea has absolutely no issues handling subnet specific domain suffixes. I just hadn't configured the d2 server correctly! Also, remember to set hostname for any static reservations.
Why is this even necessary, then? In my case, I keep my home network
split up into a couple of zones (one for clients, one for services,
one for iot and one for management) which I use different subdomains
for. One zone uses services.home.arpa
, another is clients.home.arpa
,
et c.
After debugging and testing kea in this configuration for quite a
while I gave up on having a working internal dns mapping for my dhcp
clients (RFC2136). I looked into setting up multiple kea instances on
my home router (running NixOS), but I'm still not very good at nix
so
I just accepted that I had to manually setup multiple instances of
kea, one for each zone.
Just before starting setting up these instances I realized that this was a great excus…opportunity to use Ansible! So I wrote yet another custom role for this.
It might be worth noting that below is an example and might not compile, all variables (subnets et c) needs checking to make it work.
Setup
I run FreeBSD (also due to $REASONS) on a few machines and decided that I wanted to use this machine for DHCP as well. This machine handles several network interfaces, both 'real' and virtual.
I haven't automated setting up my jails yet, so I manually created a couple of jails, one for each zone. Some of these zones have multiple subnets so I made sure to include all relevant interfaces.
# iocage create -r 13.0-RELEASE -b -n keas vnet=on boot=on bpf=on defaultrouter=192.168.100.254 ip4_addr="vnet0|192.168.100.1/24,vnet1|192.168.110.0/29,vnet2|192.168.120.0/29" resolver="nameserver 192.168.100.254" interfaces="vnet0:bridge0,vnet1|bridge110,vnet2|bridge120"
# iocage create -r 13.0-RELEASE -b -n keai vnet=on boot=on bpf=on defaultrouter=192.168.200.254 ip4_addr="vnet0|192.168.200.1/24" resolver="nameserver 192.168.200.254" interfaces="vnet0:bridge200"
bpf (and vnet) is necessary for kea to work properly, so make sure to enable that.
In case my /etc/rc.conf
is relevant, it looks something like below.
It's paraphrased but you get the idea.
# re0 carries a lot of tagged vlans (only tagged, nothing untagged, no
# "dual mode"). Please don't mix tagged and untagged traffic on one
# interface as it might bite you, see:
# https://forums.FreeBSD.org/threads/bridge-epair-not-passing-through-tagged-vlan-traffic-between-host-and-vnet-jail.71646/post-437147
ifconfig_re0="up"
cloned_interfaces="vlan100 bridge100 vlan200 bridge200"
ifconfig_vlan100="vlan 100 vlandev re0 up"
ifconfig_vlan200="vlan 200 vlandev re0 up"
ifconfig_bridge100="addm vlan100 up"
ifconfig_bridge200="addm vlan200 up"
I then copy in my public ssh key to each jail (into /root/.ssh/authorized_keys
), edit
/etc/ssh/sshd_config
to allow root login with public keys and start
ssh. (I should really automate this)
Playbook - variables
The playbook was fairly straight forward to write, but I learned a neat trick (merging configurations) which made it feel nice and pretty.
I've setup four different Kea instances so I put my default config on a group level and have overridden specifics on host level. Had I been more serious I'd set the defaults on the role instead. :-)
This is more or less a translation of the example configuration (in json) into yaml.
kea_dhcp4_default_config:
Dhcp4:
dhcp-ddns:
enable-updates: true
qualifying-suffix: "home.arpa."
interfaces-config:
interfaces: []
lease-database:
name: "/var/db/kea/dhcp4.leases"
persist: true
type: "memfile"
loggers:
- name: "kea-dhcp4"
severity: "INFO"
output_options:
- output: "/var/log/kea-dhcp4.log"
maxsize: 1048576
maxver: 8
rebind-timer: 2000
renew-timer: 1000
valid-lifetime: 4000
subnet4: []
kea_d2_default_config:
DhcpDdns:
dns-server-timeout: 100
ip-address: "127.0.0.1"
ncr-format: "JSON"
ncr-protocol: "UDP"
port: 53001
loggers:
- name: "kea-dhcp-ddns"
severity: "INFO"
output_options:
- output: "/var/log/kea-dhcp-ddns.log"
maxsize: 1048576
maxver: 8
forward-ddns: {}
reverse-ddns: {}
tsig-keys: {}
And then I override each value as necessary on a host level, note
that this is kea_dhcp4_config
and not kea_dhcp4_default_config
:
kea_dhcp4_config:
Dhcp4:
dhcp-ddns:
qualifying-suffix: "clients.home.arpa."
interfaces-config:
interfaces:
- epair0b
- epair1b
subnet4:
- subnet: "192.168.100.0/24"
option-data:
- data: "192.168.100.254"
name: routers
- data: "192.168.100.254"
name: domain-name-servers
- data: "clients.home.arpa."
name: domain-name
pools:
- pool: "192.168.100.100 - 192.168.100.200"
reservations: "{{ kea_reservations.vlan100 }}"
kea_d2_config:
DhcpDdns:
forward-ddns:
ddns-domains:
- name: "clients.home.arpa."
key-name: "tsigkey."
dns-servers:
- hostname: ""
ip-address: "192.168.200.2"
port: 53
reverse-ddns:
ddns-domains:
- name: "100.168.192.in-addr.arpa."
key-name: "tsigkey."
dns-servers:
- hostname: ""
ip-address: "192.168.200.2"
port: 53
tsig-keys:
- "{{ kea_tsig_keys.tsigkey }}"
These variables (default and host level) will be merged in a task.
Playbook - handlers
---
- name: 'kea reload'
ansible.builtin.service:
name: 'kea'
state: 'reloaded'
Playbook - templates
# kea-dhcp4.conf.j2
{% if kea_dhcp4_config %}
{{ kea_dhcp4_config | to_nice_json }}
{% endif %}
# kea-dhcp-ddns.conf.j2
{% if kea_d2_config %}
{{ kea_d2_config | to_nice_json }}
{% endif %}
Playbook - tasks
This is pretty sloppy, as there are multiple parameters that should be variable instead. I especially like two things here - the configuration merges (which I hadn't encountered before) and the template validation. The latter prevents me from breaking a working, running configuration which feels nice.
---
- name: Install dependencies
ansible.builtin.package:
name: "{{ item }}"
state: present
with_items:
- kea
- name: Enable service
ansible.builtin.service:
name: "kea"
enabled: yes
- name: Merge kea dhcp4 configuration between defaults and custom
set_fact:
kea_dhcp4_config: "{{ kea_dhcp4_default_config | combine( kea_dhcp4_config, recursive=True ) }}"
- name: Merge kea ddns (d2) configuration between defaults and custom
set_fact:
kea_d2_config: "{{ kea_d2_default_config | combine( kea_d2_config, recursive=True ) }}"
- name: Install dhcp4 configuration
ansible.builtin.template:
src: "kea-dhcp4.conf.j2"
dest: "/usr/local/etc/kea/kea-dhcp4.conf"
validate: "kea-dhcp4 -t %s"
notify:
- 'kea reload'
- name: Install d2 configuration
ansible.builtin.template:
src: "kea-dhcp-ddns.conf.j2"
dest: "/usr/local/etc/kea/kea-dhcp-ddns.conf"
validate: "kea-dhcp-ddns -t %s"
notify:
- 'kea reload'
- name: Make sure d2 is enabled in keactrl.conf
ansible.builtin.lineinfile:
path: "/usr/local/etc/kea/keactrl.conf"
regexp: "^dhcp_ddns="
line: "dhcp_ddns=yes"
notify:
- 'kea reload'
- name: Make sure dhcp4 is enabled in keactrl.conf
ansible.builtin.lineinfile:
path: "/usr/local/etc/kea/keactrl.conf"
regexp: "^dhcp4="
line: "dhcp4=yes"
notify:
- 'kea reload'
- name: Make sure dhcp6 is disabled in keactrl.conf
ansible.builtin.lineinfile:
path: "/usr/local/etc/kea/keactrl.conf"
regexp: "^dhcp6="
line: "dhcp6=no"
notify:
- 'kea reload'
- name: Make sure kea is running (if first setup)
ansible.builtin.service:
name: "kea"
state: started
Playbook - secrets
Two parts, reservations (per subnet) and tsig keys. The latter are used for authenticating with my authoritative DNS server (RFC2136).
kea_tsig_keys:
tsigkeyname:
name: "tsigkeyname"
algorithm: "goes-here"
secret: "hello world"
kea_reservations:
vlan100:
- hw-address: "00:11:22:33:44:55"
ip-address: "192.168.100.10"
- hw-address: "00:11:22:33:44:55"
ip-address: "192.168.100.11"
vlan200:
- hw-address: "00:11:22:33:44:55"
ip-address: "192.168.200.10"
- hw-address: "00:11:22:33:44:55"
ip-address: "192.168.200.11"
Conclusion
I like ansible.