Network Services at Scale

Network Services at Scale

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 (LAN 10.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 Unbound stub-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 valid auto-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 #