This guide demonstrates how to build a router using a free operating system like OpenBSD or Debian to be used for network address translation, as a stateful firewall, to filter web traffic, and more.
This guide is provided "as is" - without warranties of any kind. You are solely responsible for any consequences of following it.
See legacy/pcengines.md for previous instructions to configure the PC Engines APU platform.
The completed router configuration will enable:
- An egress Ethernet interface for Internet routing; it can be connected to a WAN uplink or cable modem
- A local wireless interface on
192.168.1.0/24 - A local Ethernet interface on
172.16.1.0/24 - A local Ethernet interface on
10.8.1.0/24 - A fourth Ethernet interface is available on the APU4
Use another computer to prepare an installer for OpenBSD or Debian.
Download the installation image - amd64/install79.img - as well as SHA256 and SHA256.sig files.
Verify the signature file and the installation image's hash:
$ cat /etc/signify/openbsd-79-base.pub
untrusted comment: openbsd 7.9 base public key
RWTSdNN9A3yvWNn7mUjXwv9DOCOUnyfuV+mq1iGPIfD+NhN8EYnEQ1at
$ signify -C -p /etc/signify/openbsd-79-base.pub -x SHA256.sig install79.img
Signature Verified
install79.img: OKInsert a USB disk. Run dmesg to identify its label. Then copy the installation file to the USB disk:
On OpenBSD:
doas dd if=install79.img of=/dev/rsd2c bs=1mOn Linux:
sudo dd if=install79.img of=/dev/sdd bs=1MDownload the latest network installation image - as well as SHA512SUMS and SHA512SUMS.sign files.
Verify the signatures file and hash of the installation image:
$ gpg SHA512SUMS.sign
gpg: assuming signed data in 'SHA512SUMS'
gpg: Signature made Sat 11 Jul 2026 01:25:53 PM PDT
gpg: using RSA key DF9B9C49EAA9298432589D76DA87E80D6294BE9B
gpg: Can't check signature: No public key
$ gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv DF9B9C49EAA9298432589D76DA87E80D6294BE9B
gpg: key 0xDA87E80D6294BE9B: public key "Debian CD signing key <debian-cd@lists.debian.org>" imported
gpg: Total number processed: 1
gpg: imported: 1
$ gpg SHA512SUMS.sign
gpg: Signature made Sat 11 Jul 2026 01:25:53 PM PDT
gpg: using RSA key DF9B9C49EAA9298432589D76DA87E80D6294BE9B
gpg: Good signature from "Debian CD signing key <debian-cd@lists.debian.org>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: DF9B 9C49 EAA9 2984 3258 9D76 DA87 E80D 6294 BE9BOpenBSD:
$ grep $(sha512 -q debian-13.6.0-amd64-netinst.iso) SHA512SUMS
ce0eeee7b51fdcdbed1e5116668c1fee27e528767bdf488e5f115a67b225e5dfd0afca1d456aaa9408ceb6b8527521ff7b6b5d62fdbe6f8c5faaf8df56a96292 debian-13.6.0-amd64-netinst.isoLinux:
$ grep $(shasum -a 512 debian-13.6.0-amd64-netinst.iso) SHA512SUMS
SHA512SUMS:ce0eeee7b51fdcdbed1e5116668c1fee27e528767bdf488e5f115a67b225e5dfd0afca1d456aaa9408ceb6b8527521ff7b6b5d62fdbe6f8c5faaf8df56a96292 debian-13.6.0-amd64-netinst.isoInsert a USB disk. Run dmesg to identify its label. Then copy the installation file to the USB disk.
OpenBSD:
doas dd if=debian-13.6.0-amd64-netinst.iso of=/dev/rsd2c bs=1mLinux:
sudo dd if=debian-13.6.0-amd64-netinst.iso of=/dev/sdd bs=1MUnplug the USB disk and plug it into the APU.
Press F10 at boot and select the USB disk.
Set the serial console parameters:
Booting from Hard Disk...
Using drive 0, partition 3.
Loading......
probing: pc0 com0 com1 mem[639K 3325M 752M a20=on]
disk: hd0+ hd1+
>> OpenBSD/amd64 BOOT 3.47
boot> stty com0 115200
boot> set tty com0
switching console to com>> OpenBSD/amd64 BOOT 3.47
boot> [Press Enter]Select the Install option:
Welcome to the OpenBSD/amd64 7.9 installation program.
(I)nstall, (U)pgrade, (A)utoinstall or (S)hell? IWhen presented with a list of network interfaces, em0 is the Ethernet port closest to the serial port:
Available network interfaces are: em0 em1 em2 em3 vlan0.Use DHCP, or configure a static IP address, default gateway, and DNS server:
Network interface to configure? (name, lladdr, '?', or 'done') [done] em0
IPv4 address for em0? (or 'autoconf' or 'none') [autoconf] 192.168.1.2
Netmask for em0? [255.255.255.0]
IPv6 address for em0? (or 'autoconf' or 'none') [none]
Available network interfaces are: em0 em1 em2 em3 vlan0.
Network interface to configure? (name, lladdr, '?', or 'done') [done]
Default IPv4 route? (IPv4 address or 'none') 192.168.1.1
add net default: gateway 192.168.1.1
DNS domain name? (e.g. 'example.com') [my.domain] local
DNS nameservers? (IP address list or 'none') [none] 192.168.1.1Configure the root password and set up a user account:
Password for root account? (will not echo)
Password for root account? (again)
Start sshd(8) by default? [yes]
Change the default console to com0? [yes]
Available speeds are: 9600 19200 38400 57600 115200.
Which speed should com0 use? (or 'done') [115200]
Setup a user? (enter a lower-case loginname, or 'no') [no] sysadm
Full name for user sysadm? [sysadm]
Password for user sysadm? (will not echo)
Password for user sysadm? (again)Select the internal mSATA disk and default options for partitioning:
Available disks are: sd0 sd1.
Which disk is the root disk? ('?' for details) [sd0] ?
sd0: ATA, SB2, SBFM naa.0000000000000000 (119.2G)
sd1: PNY, USB 2.0 FD, 1100 serial.00000000000000000000 (29.9G)
Available disks are: sd0 sd1.
Which disk is the root disk? ('?' for details) [sd0]Note
The "unused" partition (/dev/sd0c) is actually the entire disk.
Select a mirror and start the installation:
HTTP Server? (hostname, list#, 'done' or '?') cdn.openbsd.org
Server directory? [pub/OpenBSD/7.9/amd64]
Select sets by entering a set name, a file name pattern or 'all'. De-select
sets by prepending a '-', e.g.: '-game*'. Selected sets are labelled '[X]'.
[X] bsd [X] base79.tgz [X] game79.tgz [X] xfont79.tgz
[X] bsd.mp [X] comp79.tgz [X] xbase79.tgz [X] xserv79.tgz
[X] bsd.rd [X] man79.tgz [X] xshare79.tgz
Set name(s)? (or 'abort' or 'done') [done]After installation is complete, unplug the USB disk and reboot. See the OpenBSD FAQ for more information.
At the install menu, select Tab to edit boot options and replace quiet with:
console=ttyS0,115200n8Press Enter, then select an available resolution:
Undefined video mode number: 314
Press <ENTER> to see video modes available, <SPACE> to continue, or wait 30 sec
Mode: Resolution: Type:
0 F00 80x25 CGA/MDA/HGC
Enter a video mode or "scan" to scan for additional modes: 0Configure a network adapter - enp1s0 is the interface closest to the serial port.
Select Guided - use entire disk and set up LVM as the partition method. Be sure to select internal mSATA drive and not the USB disk as the installation target (usually sda).
Select Separate /home, /var, and /tmp partitions as the partitioning scheme.
During Software selection - deselect everything except SSH server.
Select the internal mSATA drive and not the USB disk as the GRUB loader target.
The following boot parameters have been appended to /etc/boot.conf by the installer and everything should just work:
stty com0 115200
set tty com0After the GRUB menu, output may get stuck at:
Loading Linux 6.1.0-23-amd64 ...
Loading initial ramdisk ...If so, reboot and press e at the GRUB menu to enter edit mode, scroll down and replace the word quiet with:
console=ttyS0,115200n8If arrow keys do not work in GRUB, try using Emacs key bindings to navigate the text field:
Control-Bto move leftControl-Fto move rightControl-Pto move upControl-Nto move down
Press Control-X to continue booting and console output should appear.
Tip
If you see Alert! /dev/sdX1 does not exist dropping to shell and reach an initramfs prompt, reboot and edit the quiet line to point to /dev/sda1 or correct partition.
Log in as root and install pending updates or switch to -current:
syspatchInstall any pending firmware updates:
fw_updateEdit /etc/doas.conf to allow the regular user to run privileged commands without a password:
permit nopass keepenv :wheel
permit nopass keepenv root
Install any needed software:
pkg_add bash zsh vim curl free pftop vnstatReboot to complete any pending updates.
Log in as root to get started.
If necessary, update GRUB by editing /etc/default/grub and removing or replacing quiet with console=ttyS0,115200n8 then update the configuration:
update-grubInstall any pending updates and necessary software:
apt update && apt -y upgrade
apt -y install lshw lsof vim zsh git sudo dnsmasq net-tools iptables tcpdump hostapd firmware-atherosOptional Change the default login shell to zsh for the primary user:
chsh -s /usr/bin/zsh sysadmOn the APU, set a local network interface address and make it permanent:
doas ifconfig em1 10.8.1.1 255.255.255.0
echo "inet 10.8.1.1 255.255.255.0" | doas tee /etc/hostname.em1Configure an OpenBSD client with DHCP by following the Networking FAQ or using a static address:
doas ifconfig em1 10.8.1.4 255.255.255.0Test it:
$ ping -c 1 10.8.1.1
PING 10.8.1.1 (10.8.1.1): 56 data bytes
64 bytes from 10.8.1.1: icmp_seq=0 ttl=255 time=0.845 msOptional Randomize MAC addresses on boot:
echo "lladdr random" | doas tee -a /etc/hostname.em0 /etc/hostname.em1 /etc/hostname.em2On the APU and on another computer, determine the interface names available:
lshw -C network | grep "logical name"On the APU, edit /etc/network/interfaces to append:
auto enp2s0
iface enp2s0 inet static
address 10.8.1.1
netmask 255.255.255.0
gateway 10.8.1.1
Where enp2s0 is the network interface one port away from the serial port.
Restart networking and bring up the interface:
service networking restart
ifup enp2s0On another Linux computer, edit /etc/network/interfaces to append:
auto eno1
iface eno1 inet static
address 10.8.1.2
netmask 255.255.255.0
gateway 10.8.1.1Then restart networking and bring up the interface:
sudo service networking restart
sudo ifup eno1Or on another OpenBSD computer, edit /etc/hostname.em0 to append:
inet 10.8.1.4 255.255.255.0It should now be possible to ping the router:
$ ping -c 1 10.8.1.1
PING 10.8.1.1 (10.8.1.1): 56 data bytes
64 bytes from 10.8.1.1: icmp_seq=0 ttl=64 time=0.519 msTo configure the wireless interface, edit /etc/network/interfaces on the APU to include:
auto wlp5s0
iface wlp5s0 inet static
address 192.168.1.1
netmask 255.255.255.0
hostapd /etc/hostapd.confReboot after verifying network connectivity.
From a client, an SSH connection to the APU should be possible, but not yet authorized:
$ ssh sysadm@10.8.1.1
The authenticity of host '10.8.1.1 (10.8.1.1)' can't be established.
ECDSA key fingerprint is SHA256:AAAAA.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '10.8.1.1' (ECDSA) to the list of known hosts.
Permission denied (publickey,password).If using a YubiKey, copy its public key to clipboard:
ssh-add -L | awk '{print $1" "$2}' | xclipOr generate a new SSH key on the client and copy it to clipboard:
ssh-keygen -f -C 'sysadm' ~/.ssh/router
xclip ~/.ssh/router.pubOn the APU, over the serial connection, as the primary user (e.g., sysadm - not root), configure SSH to accept that key by pasting it into ~/.ssh/authorized_keys:
$ mkdir ~/.ssh ; cat > ~/.ssh/authorized_keys
[Paste clipboard contents using the middle mouse button or Shift-Insert]
[Then press Control-D to save]SSH from a client will now work:
$ ssh sysadm@10.8.1.1 -i ~/.ssh/router
Host key fingerprint is SHA256:AAAAA
Linux router 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64
sysadm@router~ %Configure the connection on a client by editing ~/.ssh/config:
Host router
HostName 10.8.1.1
IdentityFile ~/.ssh/router
User sysadm
Port 22
ControlMaster auto
ControlPath ~/.ssh/master-%r@%h:%p
ControlPersist 1m
Connect using the new alias:
ssh routerDownload configuration files:
git clone https://github.com/drduh/configThe serial connection can now be terminated. Log out with Ctrl-D or exit before disconnecting, otherwise anyone can plug in the serial cable to resume the session.
Dnsmasq will provide DHCP and handle DNS for the local network(s).
Use drduh/config/dnsmasq.conf for a configuration example, including blocked domains:
sudo cp config/dnsmasq.conf /etc/dnsmasq.conf
cat config/domains/* | sudo tee -a /etc/dnsmasq.conf
sudo vim /etc/dnsmasq.confConfigure an additional blocklist:
git clone --depth 1 https://github.com/StevenBlack/hosts
sudo cp hosts/hosts /etc/dns-blocklist
sudo chmod 0744 /etc/dns-blocklistTo install dnsmasq as a service enabled on boot:
doas pkg_add dnsmasq
doas rcctl start dnsmasq
doas rcctl enable dnsmasqNote
Wireless performance is currently significantly worse on OpenBSD than Debian.
Edit /etc/hostname.athn0 to include:
inet 192.168.1.1 255.255.255.0
media autoselect mode 11n mediaopt hostap chan 11
nwid NAME wpakey "PASSWORD"Restart networking:
doas sh /etc/netstartInstall the default hostapd configuration:
cat /usr/share/doc/hostapd/examples/hostapd.conf | sudo tee -a /etc/hostapd.confOr use drduh/config/hostapd.conf:
sudo cp config/hostapd.conf /etc/hostapd.confEdit the configuration to set the network name and password.
Tip
Avoid passwords with the characters ' and ".
sudo vim /etc/hostapd.confStart hostapd:
sudo hostapd /etc/hostapd.confThe interface may need manual address assignment:
sudo ifconfig wlp5s0 192.168.1.1In order to be a router, IP forwarding must be enabled.
Enable now and on boot:
doas sysctl net.inet.ip.forwarding=1
echo "net.inet.ip.forwarding=1" | doas tee -a /etc/sysctl.confEnable now and on boot:
sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" | sudo tee --append /etc/sysctl.confSee PF - Building a Router, or use drduh/config/pf files:
doas mkdir /etc/pf
doas cp config/pf/pf.conf /etc/
doas cp config/pf/blocklist config/pf/martians config/pf/private /etc/pf/Turn PF off and back on again:
doas pfctl -d
doas pfctl -e -f /etc/pf.confOptional Use drduh/config/scripts/pf-blocklist.sh to find and block unwanted networks.
To inspect blocked traffic:
doas tcpdump -ni pflog0Use Iptables to manage a stateful firewall.
Use drduh/config/scripts/iptables.sh and edit it to your needs:
sudo cp config/scripts/iptables.sh /etc
sudo vim /etc/iptables.sh
sudo chmod +x /etc/iptables.sh
sudo /etc/iptables.shSave the firewall rules to apply them on boot:
sudo iptables-save | tee /etc/iptables/rules.v4Privoxy is a powerful proxy capable of filtering requests.
Install Privoxy:
sudo apt -y install privoxyUse drduh/config/privoxy/config and drduh/config/privoxy/user.action - or edit the configuration yourself.
sudo cp config/privoxy/config config/privoxy/user.action /etc/privoxy/Restart the service and check the log:
sudo service privoxy restart
sudo tail -f /var/log/privoxy/logfileLighttpd with mod_magnet makes for a highly capable web server which can be used to replace ad images with custom content, upload and share content on the local network, act as a captive portal, and more.
Install Lighttpd with ModMagnet:
sudo apt -y install lighttpd lighttpd-mod-magnetUse drduh/config/lighttpd/lighttpd.conf and drduh/config/lighttpd/magnet.luau - or edit the configuration yourself.
sudo cp config/lighttpd/lighttpd.conf config/lighttpd/magnet.luau /etc/lighttpd/Restart the service and check the log:
sudo service lighttpd restart
sudo cat /var/log/lighttpd/error.logFirst, install minisign or build from source.
Download the latest Linux release - dnscrypt-proxy-linux_x86_64-*.tar.gz.
curl -LfO https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/2.1.17/dnscrypt-proxy-linux_x86_64-2.1.17.tar.gz
curl -LfO https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/2.1.17/dnscrypt-proxy-linux_x86_64-2.1.17.tar.gz.minisigVerify:
$ minisign -Vm dnscrypt-proxy-*.tar.gz -P RWTk1xXqcTODeYttYMCMLo0YJHaFEHn7a3akqHlb/7QvIQXHVPxKbjB5
Signature and comment signature verified
Trusted comment: timestamp:1783957491 file:dnscrypt-proxy-linux_x86_64-2.1.17.tar.gz hashedUnpack and edit the configuration:
tar xf dnscrypt-proxy*.gz
cp config/dnscrypt-proxy.toml linux-x86_64/
cd linux-x86_64/
vim dnscrypt-proxy.tomlOptional Download and configure a hosts blocklist:
git clone --depth 1 https://github.com/DNSCrypt/dnscrypt-proxy
cd dnscrypt-proxy/utils/generate-domains-blocklist
python3 generate-domains-blocklist.py > blocklist-$(date +%F).txt
cp blocklist-$(date +%F).txt ~/linux-x86_64/blocklist.txtInstall and start the service:
sudo ./dnscrypt-proxy -service install
sudo ./dnscrypt-proxy -service start
tail -f dnscrypt.logTo confirm the firewall is configured correctly, run port scans from an internal and external hosts, for example:
nmap -v -A -T4 192.168.1.1 -PnTo view blocked packets, tail the system message buffer on Linux:
$ sudo dmesg -wH
[Jul 1 12:00] DROPIN>IN=enp1s0 OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:00:00 SRC=192.168.1.10 DST=192.168.1.1 LEN=64 TOS=0x00 PREC=0x00 TTL=64 ID=29501 DF PROTO=TCP SPT=43228 DPT=554 WINDOW=16384 RES=0x00 SYN URGP=0
[...]On OpenBSD, blocked packets will be sent to the PF log interface:
$ doas tcpdump -ni pflog0
tcpdump: listening on pflog0, link-type PFLOG
12:00:00.000000 192.168.1.10.40770 > 192.168.1.1.1720: S 3331100898:3331180098(0) win 29200 <mss 1460,sackOK,timestamp 232580000 0,nop,wscale 7> (DF)
[...]Install a USB camera and configure Motion to detect and monitor physical access.
(Linux only) Increase system entropy with a hardware device like OneRNG.
Check open network ports with doas fstat | grep net and doas netstat -a -n -p udp -p tcp
Check running processes and sessions with ps -A and last
Pay attention to OpenBSD errata and apply security fixes periodically with doas syspatch
OpenBSD releases occur approximately every six months - follow current snapshots for faster updates by periodically running doas sysupgrade -s to reboot and install updates.
Check temperatures with sysctl hw.sensors or configure sensorsd.
Check open ports and listening programs with sudo lsof -Pni and sudo netstat -npl
Check running processes and logged-in users with ps -eax and last -F
Pay attention to Debian security advisories and run sudo apt update && sudo apt upgrade periodically or configure unattended upgrades.
Install and enable SELinux:
sudo apt -y install selinux-basics selinux-policy-default
sudo selinux-activate
sudo rebootOr, install and enable AppArmor, then reboot:
sudo apt -y install apparmor apparmor-profiles apparmor-utils
sudo mkdir -p /etc/default/grub.d
echo 'GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT apparmor=1 security=apparmor"' | sudo tee /etc/default/grub.d/apparmor.cfg
sudo update-grub && sudo rebootInstall and enable Firejail:
sudo apt -y install firejail firejail-profiles
sudo firecfgSee also Debian SSD Optimizations.