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