Skip to content

Instantly share code, notes, and snippets.

@Vicfred
Last active February 28, 2026 20:41
Show Gist options
  • Select an option

  • Save Vicfred/21d5d1289e277f13583f4ded274962d4 to your computer and use it in GitHub Desktop.

Select an option

Save Vicfred/21d5d1289e277f13583f4ded274962d4 to your computer and use it in GitHub Desktop.
OpenBSD Router

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.


0) Assumptions and goals

Topology (adjust names if yours differ):

  • em0 = WAN (behind CGNAT, upstream router gives it DHCP)
  • em1 = LAN1 172.16.0.0/24 (router IP 172.16.0.1)
  • em2 = LAN2 172.16.1.0/24 (router IP 172.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

1) Fresh install checklist

1.1 Install OpenBSD (basic)

During install:

  • Set a strong root password
  • Configure em0 via DHCP (typical for WAN)
  • Don’t overthink sets; default sets are fine
  • Enable sshd if you want remote management

After first boot, log in as root.

1.2 Identify NIC names (don’t assume)

ifconfig -a | sed -n '1,200p'
dmesg | egrep -i 'em[0-9]|i211|igc|re0|bge|ure'

Confirm which NIC is WAN vs LANs.


2) Base system config

2.1 Hostname

echo "puffy" > /etc/myname
hostname puffy

2.2 Enable IP forwarding (router mode)

sysctl 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.conf

3) Network interfaces

OpenBSD interface config lives in /etc/hostname.IF.

3.1 WAN: em0 via DHCP

/etc/hostname.em0

dhcp
-inet6

Bring it up:

sh /etc/netstart em0

3.2 LAN1: em1 static

/etc/hostname.em1

inet 172.16.0.1 255.255.255.0
up
-inet6

3.3 LAN2: em2 static

/etc/hostname.em2

inet 172.16.1.1 255.255.255.0
up
-inet6

Apply:

sh /etc/netstart em1
sh /etc/netstart em2
ifconfig em1
ifconfig em2

4) pf firewall + NAT

4.1 Enable pf at boot

/etc/rc.conf.local

pf=YES

Load now:

rcctl enable pf
rcctl start pf

4.2 /etc/pf.conf

/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

5) DHCP server (dhcpd) on LANs

5.1 Configure dhcpd

/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;
}

5.2 Enable dhcpd with your preferred style (no dhcpd.interfaces)

/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

6) DNS resolver (unbound) for the LAN

6.1 Make the router use itself for DNS

/etc/resolv.conf

nameserver 127.0.0.1
lookup file bind

6.2 Minimal unbound config with local domain

/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 || true

Quick test:

drill magi.cyberia.home.arpa @172.16.0.1
drill openbsd.org @172.16.0.1

6.5) SSH daemon (sshd) hardening

This router is infrastructure, not a general-purpose box. SSH should be key-only, non-standard port, single user, no X11, no root login.

6.5.1 Edit sshd configuration

Open the SSH daemon config:

vi /etc/ssh/sshd_config

Append 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

6.5.2 Enable and restart sshd

Enable at boot (usually already enabled, but explicit is good):

rcctl enable sshd

Restart to apply changes:

rcctl restart sshd
rcctl check sshd

7) Make sure clients actually use the router

  • Clients should get:

    • Router/gateway: 172.16.0.1 or 172.16.1.1
    • DNS: same router IP on their LAN
    • Search domain: cyberia.home.arpa

From a client on LAN1:

dig +short openbsd.org @172.16.0.1

8) Operational commands you’ll use constantly

Service control

rcctl ls on
rcctl restart pf
rcctl restart dhcpd
rcctl restart unbound

Inspect firewall

pfctl -sr
pfctl -sn
pfctl -sa | sed -n '1,200p'
tcpdump -ni em0
tcpdump -ni em1

Logs

tail -f /var/log/daemon
tail -f /var/log/messages

9) “Tighten later” hardening knobs (optional but common)

Once 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 pf tables 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.)


10) Backup list (what to copy off the router)

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_config

A 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment