Synopsis #
This chapter describes building reliable, scalable network services on OpenBSD: authoritative DNS with nsd(8) configured via nsd.conf(5), validating recursive DNS with unbound(8) configured via unbound.conf(5), IPv4 address allocation with dhcpd(8) configured via dhcpd.conf(5), and time services with ntpd(8) configured via ntpd.conf(5). Service lifecycle management uses rcctl(8). Packet filtering allowances live in pf.conf(5) and are applied with pfctl(8). Use these patterns for branch offices, campus networks, and PoPs where you operate DNS, DHCP, and NTP as first-class, redundant services.
Design Considerations #
- Separation of roles. Bind authoritative and recursive DNS to different IP addresses. A common pattern is NSD listening on a public/WAN address (or a dedicated service VIP) and Unbound listening on LAN addresses.
- Redundancy. Use at least two servers per critical service. Combine with CARP and pfsync from the HA chapter for failover of service VIPs.
- Security posture. Permit UDP/TCP 53 from the Internet only to authoritative servers. Restrict recursion to inside prefixes. Enable DNSSEC validation in Unbound.
- Operational boundaries. NSD does not support dynamic updates; keep zone management explicit and version-controlled. Unbound is not authoritative; use stub zones to prefer local authoritative answers when both run on the same host.
- IPv6. Provide AAAA records in authoritative zones and ensure recursive service answers both A and AAAA. Address assignment for IPv6 prefers RA (see the IPv6 chapter); DHCPv6 is out of scope here.
- Time service. DNSSEC validation requires correct time. Ensure
ntpd(8)
is running on resolvers and that clients have a local NTP source.
Configuration #
Assumptions for examples:
- Interfaces:
em0
(WAN),em1
(LAN10.10.10.0/24
). - Service IPs:
203.0.113.53
(authoritative DNS on WAN),10.10.10.1
(recursive DNS and NTP on LAN). - Authoritative zone:
example.com.
served by NSD. - Local recursion for LAN clients via Unbound.
1) Authoritative DNS with nsd(8) #
Bind NSD to the public address and serve example.com.
from a local zone file. Control socket is enabled for safe reloads.
## /etc/nsd.conf — NSD authoritative service on WAN address
server:
ip-address: 203.0.113.53
hide-version: yes
do-ip4: yes
do-ip6: no
server-count: 1
zonesdir: "/var/nsd/zones"
# control socket for nsd-control(8)
control-enable: yes
zone:
name: "example.com"
zonefile: "example.com.zone"
Create the zone file (minimal example; expand as required). The SOA and NS records must be correct for production.
## /var/nsd/zones/example.com.zone — minimal zone
$ORIGIN example.com.
$TTL 300
@ IN SOA ns1.example.com. hostmaster.example.com. (
2025010101 ; serial (YYYYMMDDNN)
3600 ; refresh
600 ; retry
1209600 ; expire
300 ; minimum
)
IN NS ns1.example.com.
ns1 IN A 203.0.113.53
www IN A 203.0.113.80
api IN A 203.0.113.81
First-time control key setup and service start:
# nsd-control-setup
# Generate control keys/certs under /var/nsd/etc; one-time
# rcctl enable nsd
# Start on boot
# rcctl start nsd
# Launch now
# nsd-control reload
# Validate and load the zone
2) Validating recursive DNS with unbound(8) #
Unbound will listen on LAN, serve only inside clients, and validate DNSSEC. When NSD and Unbound share a host, forward a local zone to NSD via a stub to avoid conflicts on port 53 by running NSD on WAN and Unbound on LAN.
## /var/unbound/etc/unbound.conf — LAN recursion with validation
server:
interface: 127.0.0.1
interface: 10.10.10.1
access-control: 10.10.10.0/24 allow
access-control: 127.0.0.0/8 allow
# Hardening and privacy
hide-identity: yes
hide-version: yes
qname-minimisation: yes
harden-referral-path: yes
harden-glue: yes
prefetch: yes
# DNSSEC
auto-trust-anchor-file: "/var/unbound/db/root.key"
# Root priming; Unbound can fetch root NS automatically if needed
root-hints: "/var/unbound/db/root.hints"
# Prefer local authoritative answers (optional internal zone served by NSD)
stub-zone:
name: "corp.example."
stub-addr: 127.0.0.1@5353
If you need Unbound to prefer a local zone while NSD keeps port 53 on WAN, bind NSD additionally to 127.0.0.1 port 5353
and serve corp.example.
there (example below). Otherwise omit the stub-zone
.
## Excerpt — additional NSD listener for an internal zone (loopback)
server:
# ... existing server block ...
ip-address: 127.0.0.1@5353
zone:
name: "corp.example"
zonefile: "corp.example.zone"
Initialize Unbound control and start the resolver:
# unbound-control-setup
# Generate control keys/certs under /var/unbound/etc; one-time
# rcctl enable unbound
# rcctl start unbound
# unbound-control status
# Confirm validator is ready and loaded
3) DHCP for IPv4 with dhcpd(8) #
Serve 10.10.10.0/24
, advertise the local resolver and default gateway, and provide a static mapping example.
## /etc/dhcpd.conf — IPv4 DHCP on LAN
option domain-name "corp.example";
option domain-name-servers 10.10.10.1;
option routers 10.10.10.1;
option subnet-mask 255.255.255.0;
default-lease-time 3600;
max-lease-time 7200;
authoritative;
subnet 10.10.10.0 netmask 255.255.255.0 {
range 10.10.10.100 10.10.10.200;
# Static reservation example
host printer-01 {
hardware ethernet 00:11:22:33:44:55;
fixed-address 10.10.10.50;
}
}
Enable the daemon and bind it to the LAN interface:
# rcctl set dhcpd flags em1
# Specify served interfaces (required)
# rcctl enable dhcpd
# rcctl start dhcpd
# tail -n 20 /var/db/dhcpd.leases
# Inspect active leases
For IPv6 address assignment, prefer Router Advertisements with
rad(8)
as shown in the IPv6 chapter.
4) Time service with ntpd(8) #
Provide an internal NTP source on the LAN and maintain correct time on the server itself.
## /etc/ntpd.conf — serve LAN and sync from upstream pool
listen on 10.10.10.1
servers pool.ntp.org
# rcctl enable ntpd
# rcctl start ntpd
# ntpctl -s status
# Report synchronization state
5) PF allowances for DNS, DHCP, and NTP #
Permit external queries to NSD on WAN, recursive service on LAN, DHCP on LAN, and NTP for clients.
## /etc/pf.conf — service allowances (merge into your policy)
set skip on lo
wan = "em0"
lan = "em1"
# Base policy
block all
# Authoritative DNS on WAN (NSD)
pass in on $wan proto { udp tcp } to 203.0.113.53 port 53 keep state
# Recursive DNS on LAN (Unbound)
pass in on $lan proto { udp tcp } to 10.10.10.1 port 53 keep state
# DHCP on LAN (server replies from 67 to client port 68)
pass in on $lan proto udp from port 68 to port 67 keep state
pass out on $lan proto udp from port 67 to port 68 keep state
# NTP on LAN
pass in on $lan proto udp to 10.10.10.1 port 123 keep state
# Resolver and NSD outbound as needed
pass out on $wan proto { udp tcp } to any port { 53, 123 } keep state
Reload after editing:
# pfctl -f /etc/pf.conf
# Load rules atomically
Verification #
- Authoritative zone served by NSD (from a remote host or the firewall itself), using drill(1):
$ drill @203.0.113.53 example.com SOA
# Expect the SOA you configured
$ drill @203.0.113.53 www.example.com A
# Expect 203.0.113.80
- Recursive resolution with DNSSEC validation (from a LAN client):
$ drill @10.10.10.1 cloudflare.com A
# General recursion works
$ drill -S @10.10.10.1 dnssec-failed.org
# Should return a validation failure (bogus), proving DNSSEC is active
- Unbound and NSD control:
# unbound-control list_stubs
# Confirm any stub-zones are loaded
# nsd-control stats
# Basic qps/counters for authoritative service
- DHCP leases and client config:
$ grep dhcp /var/log/messages | tail -n 20
# Server activity
$ cat /var/db/dhcpd.leases | tail -n 10
# Lease database updates
- NTP status:
$ ntpctl -s peers
# Upstream peers and offsets
$ ntpctl -s status
# Overall sync status
Troubleshooting #
- Port conflict on 53. NSD and Unbound cannot both bind the same IP:port. Bind NSD to the WAN address and Unbound to LAN addresses, or move one to
127.0.0.1@5353
and use Unboundstub-zone
for the internal domain. - Open resolver exposed. If outside hosts can recurse, restrict Unbound with
access-control
to inside prefixes only and confirm PF denies WAN access to the recursive listener. - DNSSEC failures. Validation requires correct system time and a current trust anchor. Ensure
ntpd(8)
is synchronized and that Unbound has a validauto-trust-anchor-file
. - Authoritative zone not loading. Check
nsd-control reload
output and syslog for syntax errors. Validate SOA/NS correctness and serial increments. - DHCP not serving. Confirm
rcctl set dhcpd flags <lan-ifaces>
and that PF allows UDP 67/68 on the LAN. Inspect/var/db/dhcpd.leases
and logs for pool exhaustion or MAC mismatches. - Clients ignore NTP. Ensure LAN clients point to
10.10.10.1
for NTP and that PF allows UDP 123 from LAN to the server.
See Also #
- Networking
- OpenBGPD
- Related: High Availability and State Replication
- Related: Telemetry, Logging, and Flow Export
- Related: IPv6 at Scale