Below is a from-scratch OpenBSD router runbook you can keep as your “fresh install checklist”. It assumes your apu2c with 3 Intel i211AT NICs and your usual style: OpenBSD + rcctl, and you prefer dhcpd_flags="em1 em2" in rc.conf.local (no /etc/dhcpd.interfaces). I’ll also use your LAN search domain: cyberia.home.arpa.
Topology (adjust names if yours differ):
em0= WAN (behind CGNAT, upstream router gives it DHCP)em1= LAN1172.16.0.0/24(router IP172.16.0.1)em2= LAN2172.16.1.0/24(router IP172.16.1.1)
Services:
- NAT + firewall with
pf - DHCP on LANs with
dhcpd - Local DNS resolver + cache with
unbound - Optional: static hostnames and local zones for
cyberia.home.arpa
During install:
- Set a strong root password
- Configure
em0via DHCP (typical for WAN) - Don’t overthink sets; default sets are fine
- Enable
sshdif you want remote management
After first boot, log in as root.
ifconfig -a | sed -n '1,200p'
dmesg | egrep -i 'em[0-9]|i211|igc|re0|bge|ure'Confirm which NIC is WAN vs LANs.
echo "puffy" > /etc/myname
hostname puffysysctl net.inet.ip.forwarding=1
echo "net.inet.ip.forwarding=1" >> /etc/sysctl.conf(Disable IPv6 forwarding if you don't need it)
sysctl net.inet6.ip6.forwarding=0
echo "net.inet6.ip6.forwarding=0" >> /etc/sysctl.confOpenBSD interface config lives in /etc/hostname.IF.
/etc/hostname.em0
dhcp
-inet6Bring it up:
sh /etc/netstart em0/etc/hostname.em1
inet 172.16.0.1 255.255.255.0
up
-inet6/etc/hostname.em2
inet 172.16.1.1 255.255.255.0
up
-inet6Apply:
sh /etc/netstart em1
sh /etc/netstart em2
ifconfig em1
ifconfig em2/etc/rc.conf.local
pf=YESLoad now:
rcctl enable pf
rcctl start pf/etc/pf.conf
# -------------------------------------------------------------------------
# PF ruleset: 2 LANs (em1/em2) -> NAT out WAN (em0)
# - Default deny
# - Quiet WAN (drop inbound)
# - NAT for both LANs
# - Force clients to use router Unbound (block outbound 53/853 from LANs)
# - Isolate LAN A and LAN B
# - Unbound upstreams: explicit DNS-over-TLS (TCP/853) to chosen providers
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
# LAN ISOLATION (Crucial Step)
# -------------------------------------------------------------------------
# Explicitly block traffic between LANs.
# Since we have "block all" above, we just need to make sure our PASS rules
# below don't accidentally allow it.
# The safest way is to use "! <lan_nets>" in the pass rules.
# -------------------------------------------------------------------------
# MACROS
# -------------------------------------------------------------------------
lan_if_a = "em1"
lan_if_b = "em2"
lan_ifs = "{ em1 em2 }"
lan_net_a = "172.16.0.0/24"
lan_net_b = "172.16.1.0/24"
wan_if = "em0"
# 80 -> HTTP
# 443 -> HTTPS
# 853 -> DoT: DNS over TLS
# 873 -> Rsync
# 1119 -> Blizzard client
# 1935 -> RTMP
# 3724 -> WoW login
tcp_services = "{ 80, 443, 873, 1119, 1935, 3724 }"
# 53 -> UDP (Only allowed to the router)
# 123 -> NTP
# 443 -> QUIC (HTTPS-like)
# 3478 -> Tailscale STUN (Session Traversal Utilities for NAT)
# 21027 -> Syncthing Local Discovery (Broadcast in LAN only)
# 41641 -> Tailscale Data-plane
# 51820 -> Wireguard
udp_services = "{ 123, 443, 3478, 41641, 51820 }"
table <lan_nets> const { $lan_net_a $lan_net_b }
table <dot_upstreams> const { 9.9.9.9 194.242.2.4 }
# -------------------------------------------------------------------------
# OPTIONS & NORMALIZATION
# -------------------------------------------------------------------------
block quick inet6
set skip on lo
match in all scrub (no-df)
set block-policy drop
antispoof quick for $wan_if
antispoof quick for $lan_ifs
# -------------------------------------------------------------------------
# NAT (LAN -> WAN)
# -------------------------------------------------------------------------
match out on $wan_if inet from <lan_nets> to any nat-to ($wan_if)
# -------------------------------------------------------------------------
# DEFAULT POLICY
# -------------------------------------------------------------------------
block all
block in quick on $wan_if
# -------------------------------------------------------------------------
# LAN -> Router (services you provide)
# -------------------------------------------------------------------------
# SSH to router from LANs
pass in on $lan_ifs inet proto tcp from <lan_nets> to self port 3322 keep state
# DNS to router (Unbound on the router)
pass in on $lan_ifs inet proto udp from <lan_nets> to self port 53 keep state
pass in on $lan_ifs inet proto tcp from <lan_nets> to self port 53 keep state
# Syncthing local discovery (broadcast per LAN)
pass in quick on $lan_if_a inet proto udp from $lan_net_a to 172.16.0.255 port 21027 no state
pass in quick on $lan_if_b inet proto udp from $lan_net_b to 172.16.1.255 port 21027 no state
# -------------------------------------------------------------------------
# LAN -> WAN (client egress)
# -------------------------------------------------------------------------
# Allow LAN clients to ping out (and PMTU stuff)
pass in on $lan_ifs inet proto icmp from <lan_nets> to !<lan_nets> keep state
# Allow router to ping LAN hosts
pass out on $lan_ifs inet proto icmp from self to <lan_nets> keep state
# Allow clients to ping the router
pass in on $lan_ifs inet proto icmp from <lan_nets> to self keep state
# HTTPS / RTMP(S) etc
pass in on $lan_ifs inet proto tcp from <lan_nets> to !<lan_nets> port $tcp_services keep state
pass in on $lan_ifs inet proto udp from <lan_nets> to !<lan_nets> port $udp_services keep state
# -------------------------------------------------------------------------
# DNS hardening hooks
# -------------------------------------------------------------------------
# Allow ONLY DoT from the router to known upstreams (common):
pass out quick on $wan_if inet proto tcp from ($wan_if) to <dot_upstreams> port 853 keep state
block out quick on $wan_if inet proto tcp from ($wan_if) to any port 853
# Router's own outbound (updates, DoT, etc.)
pass out on $wan_if inet from ($wan_if) to any keep state
Validate + load:
pfctl -nf /etc/pf.conf
pfctl -f /etc/pf.conf
pfctl -sr
pfctl -sn/etc/dhcpd.conf
option domain-name "cyberia.home.arpa";
option domain-search "cyberia.home.arpa";
default-lease-time 3600;
max-lease-time 7200;
subnet 172.16.0.0 netmask 255.255.255.0 {
option routers 172.16.0.1;
option domain-name-servers 172.16.0.1;
range 172.16.0.100 172.16.0.199;
host magi {
hardware ethernet 9a:64:e9:1c:5f:51;
fixed-address 172.16.0.10;
}
host megumi {
hardware ethernet d2:54:5b:1e:f3:bd;
fixed-address 172.16.0.20;
}
host sylvanas {
hardware ethernet e4:1f:d5:cf:65:f2;
fixed-address 172.16.0.30;
}
host libreelec {
hardware ethernet 2c:cf:67:51:75:2c;
fixed-address 172.16.0.40;
}
host haruko {
hardware ethernet 3e:2b:ec:56:9c:0b;
fixed-address 172.16.0.50;
}
host shizuka {
hardware ethernet ee:91:2f:a0:cf:4f;
fixed-address 172.16.0.60;
}
}
subnet 172.16.1.0 netmask 255.255.255.0 {
option routers 172.16.1.1;
option domain-name-servers 172.16.1.1;
range 172.16.1.100 172.16.1.199;
}
/etc/rc.conf.local
dhcpd_flags="em1 em2"Enable + start:
rcctl enable dhcpd
rcctl start dhcpd
rcctl check dhcpd
tail -n 200 /var/log/daemon/etc/resolv.conf
nameserver 127.0.0.1
lookup file bind
/var/unbound/etc/unbound.conf (OpenBSD’s typical path)
server:
interface: 127.0.0.1
interface: 172.16.0.1
interface: 172.16.1.1
# DNSSEC trust anchor
auto-trust-anchor-file: "/var/unbound/db/root.key"
do-ip4: yes
do-ip6: no
access-control: 127.0.0.0/8 allow
access-control: 172.16.0.0/24 allow
access-control: 172.16.1.0/24 allow
access-control: 0.0.0.0/0 refuse
# Hardening / privacy tweaks
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
qname-minimisation: yes
prefetch: yes
# Optional local names; keep or delete as you like
local-zone: "cyberia.home.arpa." static
local-data: "puffy.cyberia.home.arpa. IN A 172.16.0.1"
local-data-ptr: "172.16.0.1 puffy.cyberia.home.arpa."
local-data: "puffy.cyberia.home.arpa. IN A 172.16.1.1"
local-data-ptr: "172.16.1.1 puffy.cyberia.home.arpa."
local-data: "magi.cyberia.home.arpa. IN A 172.16.0.10"
local-data-ptr: "172.16.0.10 magi.cyberia.home.arpa."
local-data: "megumi.cyberia.home.arpa. IN A 172.16.0.20"
local-data-ptr: "172.16.0.20 megumi.cyberia.home.arpa."
local-data: "sylvanas.cyberia.home.arpa. IN A 172.16.0.30"
local-data-ptr: "172.16.0.30 sylvanas.cyberia.home.arpa."
local-data: "libreelec.cyberia.home.arpa. IN A 172.16.0.40"
local-data-ptr: "172.16.0.40 libreelec.cyberia.home.arpa."
local-data: "haruko.cyberia.home.arpa. IN A 172.16.0.50"
local-data-ptr: "172.16.0.50 haruko.cyberia.home.arpa."
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 9.9.9.9@853 # Quad9
forward-addr: 194.242.2.4@853 # Mullvad
#forward-addr: 1.1.1.1@853 # Cloudflare
/etc/rc.conf.local
dhcpd_flags="em1 em2"
resolvd_flags=NO
slaacd_flags=NO
unbound_flags=
Enable + start:
rcctl enable unbound
rcctl start unbound
rcctl check unbound
unbound-control status 2>/dev/null || trueQuick test:
drill magi.cyberia.home.arpa @172.16.0.1
drill openbsd.org @172.16.0.1This router is infrastructure, not a general-purpose box. SSH should be key-only, non-standard port, single user, no X11, no root login.
Open the SSH daemon config:
vi /etc/ssh/sshd_configAppend the following at the very end of the file (do not replace earlier defaults unless you explicitly want to):
Protocol 2
Port 3322
Ciphers aes256-ctr,aes192-ctr,aes128-ctr
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
ChallengeResponseAuthentication no
PermitEmptyPasswords no
AllowUsers vicfred
MaxAuthTries 3
MaxStartups 10:30:60
LoginGraceTime 20
X11Forwarding no
LogLevel VERBOSE
HostKey /etc/ssh/ssh_host_ed25519_key
PubkeyAcceptedKeyTypes ssh-ed25519
HostKeyAlgorithms ssh-ed25519
Enable at boot (usually already enabled, but explicit is good):
rcctl enable sshdRestart to apply changes:
rcctl restart sshd
rcctl check sshd-
Clients should get:
- Router/gateway:
172.16.0.1or172.16.1.1 - DNS: same router IP on their LAN
- Search domain:
cyberia.home.arpa
- Router/gateway:
From a client on LAN1:
dig +short openbsd.org @172.16.0.1rcctl ls on
rcctl restart pf
rcctl restart dhcpd
rcctl restart unboundpfctl -sr
pfctl -sn
pfctl -sa | sed -n '1,200p'
tcpdump -ni em0
tcpdump -ni em1tail -f /var/log/daemon
tail -f /var/log/messagesOnce it’s working, you typically tighten:
- Restrict LAN-to-LAN (isolation)
- Only allow DNS to leave LAN via router (block port 53/853 to internet except router)
- Add
pftables for bogons/bad hosts - Rate-limit inbound to router management services
(We can add those rules cleanly after you confirm what isolation you want between 172.16.0.0/24 and 172.16.1.0/24.)
This is the “save me next reinstall” list:
/etc/myname
/etc/hosts
/etc/hostname.em0
/etc/hostname.em1
/etc/hostname.em2
/etc/sysctl.conf
/etc/rc.conf.local
/etc/pf.conf
/etc/dhcpd.conf
/var/unbound/etc/unbound.conf
/etc/resolv.conf
/etc/ssh/sshd_configA simple manual backup:
mkdir -p /root/router-backup
cp -a \
/etc/myname /etc/hosts /etc/hostname.* /etc/sysctl.conf /etc/rc.conf.local \
/etc/pf.conf /etc/dhcpd.conf /etc/resolv.conf \
/var/unbound/etc/unbound.conf \
/root/router-backup/ /etc/ssh/sshd_config
tar -C /root -czf /root/router-backup.tgz router-backup
ls -lh /root/router-backup.tgz