OpenWRT and debian hardening
Home Network Hardening — OpenWrt 25.x + Debian 12-13
Companion script for the GNS3 demo video. Each block shows the command(s) and a one-line note on what it does. This is a advanced version of security in depth. OpenWRT has been chosen due to its excellent security and flexibility. All configurations shown in video and this script can be preformed in the OpenWRTs GUI called Luci, and in the Debian GUI environment.
Assumed topology
- OpenWrt router LAN IP:
192.168.1.1/24on thelaninterface - OpenWRT router WAN IP:
192.168.42.XXXon thewaninterface - Management VLAN / LAN network:
192.168.1.0/24 - Debian 12-13 box: any address inside
192.168.1.0/24, single NIC on LAN - All commands are run as
rootunless noted
Lockout warning: Before disabling Dropbear or password auth on either box, confirm key-based login already works from a second terminal. Do not close your existing session until you have verified the new one.
1. OpenWrt 25.x — UCI hardening
1.1 Update package lists and install essentials & set root password
apk update
apk add openssh-server openssh-keygen openssh-sftp-server vim
passwd # set root password - min 15 characters in accordance to the NIST.SP.800-63B-4 (2026) Policy implemented in LinuxRefreshes the package index and installs the OpenSSH server (we will swap it in for Dropbear later) plus a usable editor. OpenWrt 25.x ships with apk as the package manager — opkg is gone.
1.2 Firewall — deny all incoming on WAN, allow LAN
The OpenWrt default already drops WAN input, but we make it explicit and verify.
uci set firewall.@zone[0].input='ACCEPT' # lan zone
uci set firewall.@zone[0].forward='ACCEPT'
uci set firewall.@zone[0].output='ACCEPT'
uci set firewall.@zone[1].input='DROP' # wan zone
uci set firewall.@zone[1].forward='DROP'
uci set firewall.@zone[1].output='ACCEPT'
uci set firewall.@zone[1].masq='1'
uci set firewall.@zone[1].mtu_fix='1'
uci commit firewallForces the WAN zone to DROP (silent discard) all unsolicited inbound traffic and forwarded traffic. LAN remains fully open inside the trusted network. DROP is preferred over REJECT for stealth — the box stays silent instead of returning ICMP unreachables.
1.3 Stealth — disable WAN ping responses
idx=$(uci show firewall | awk -F'[][]' "/name='Allow-Ping'/{print \$2; exit}")
uci set firewall.@rule[$idx].enabled='0'
uci commit firewall
/etc/init.d/firewall restartFinds the default Allow-Ping rule (which permits ICMP echo on WAN) and disables it. The router will no longer reply to pings from the internet — confirms WAN stealth.
1.4 LuCI — bind to LAN only, force HTTPS, block from WAN
apk add luci-ssl-openssl px5g-mbedtls # ensure TLS cert tooling is present
uci set uhttpd.main.listen_http='192.168.1.1:80' # bind plaintext to LAN IP only
uci set uhttpd.main.listen_https='192.168.1.1:443' # bind TLS to LAN IP only
uci set uhttpd.main.redirect_https='1' # force HTTP -> HTTPS redirect
uci commit uhttpd
/etc/init.d/uhttpd restartLuCI is now reachable only via https://192.168.1.1 from inside the LAN. The WAN firewall drop already blocks external access; this binds the daemon to the LAN IP so it cannot even be served on other interfaces — defence in depth.
1.5 SSH — generate keys on your workstation (do this first! on the debian box)
Run this on your laptop / management workstation, not on the router.
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/openwrt_admin -C "openwrt-admin"Creates an Ed25519 keypair with 100 KDF rounds for the OpenWrt admin login. Ed25519 is small, fast, and immune to many RSA-era footguns.
1.6 SSH — install your public key on OpenWrt
Still from your workstation, while Dropbear is still active.
ssh-copy-id -i ~/.ssh/openwrt_admin.pub [email protected]Copies your public key into /etc/dropbear/authorized_keys (we will copy this to openssh in next step as dropbear will be disabled) on the router. Verify you can log in with the key before continuing.
Now let’s check if the key works and allow nopass login into the OpenWRT box
ssh -i ~/.ssh/openwrt-admin [email protected]1.7 SSH — harden /etc/ssh/sshd_config
# Generate a fresh Ed25519 host key, remove RSA/ECDSA hosts keys
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" -a 100
rm -f /etc/ssh/ssh_host_rsa_key* /etc/ssh/ssh_host_ecdsa_key*
# Re-write the sshd_config
Port 22
ListenAddress 192.168.1.1
AddressFamily inet
HostKey /etc/ssh/ssh_host_ed25519_key
LoginGraceTime 30
PermitRootLogin prohibit-password
MaxAuthTries 3
MaxSessions 3
PubkeyAuthentication yes
# Stronger key exchange (post-quantum + modern only)
KexAlgorithms curve25519-sha256,[email protected]
Ciphers [email protected],[email protected]
MACs [email protected],[email protected]
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
UsePAM no
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2Key points: SSH only listens on 192.168.1.1 (no WAN exposure even if firewall ever fails), Ed25519-only host key, key-based auth only, root may log in with a key only, modern Kex/Cipher/MAC suites.
1.8 SSH — disable Dropbear, enable OpenSSH
Make sure you are connected via direct terminal (console) or have an active SSH session with OpenWrt before running these commands. If anything goes wrong with the
sshd_configfile written in §1.7, OpenSSH will refuse to start, and once Dropbear is disabled you will not be able to regain remote (SSH) access to the router. Keep your current session open until §1.9 confirms the new daemon works OR as in video have direct terminal connection to OpenWRT.
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cp /etc/dropbear/authorized_keys /root/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chown root:root ~/.ssh ~/.ssh/authorized_keysCreates the ‘.ssh’ folder and copies the authorized_keys file from dropbear to it. Sets the permision for access to root only, folder rwx, file rw.
/etc/init.d/dropbear stop
/etc/init.d/dropbear disable
/etc/init.d/sshd enable
/etc/init.d/sshd startBrings up OpenSSH on port 22 (LAN IP only) and removes Dropbear from the boot sequence.
Due to the dropbear change to openssh the hosts keys have changed, we need to add the new public ket to the management machine to log in, we will do this in next step.
1.9 OpenWrt — final verification
Remove the old public key from the known_hosts file. New one will be added during first login.
vim ~/.ssh/known_hostsRemove the first and only line in the file, which will be the old public key for the OpenWRT router, used the ‘dd’ key command in vim and then same/exit with ‘:wq’.
# from your workstation
ssh -i ~/.ssh/openwrt_admin [email protected] "uname -a" # should log in cleanlyConfirms key-based access works from LAN.
On the VPCS (PC1)
ip dhcp # gets adn IP from the NAT subnet
ping 192.168.42.1 # NATs default gate
ping 192.168.42.XXX # OpenWRT's wan interface - XXX change to what ever the last octed of the IP is on OpenWRTConfirm connection to NAT and that stealth works on OpenWRT.
2. Debian 12-13 — UFW + SSH + base hardening
All commands in this section need to be run as root.
2.1 Become root and update the system
# You are logged in as the install user (e.g. 'admin') — non-sudo by default.
whoami # confirm: should NOT be root
sudo su - # enter the root password set during Debian install
whoami # confirm: should now be 'root'# Patch the system and install the hardening packages (sudo included).
apt update && apt full-upgrade -y
apt install -y ufw openssh-server fail2ban unattended-upgrades neovimsu - (with the trailing dash) starts a full login shell as root, loading roots environment — the standard way to become root on a Debian box with no sudo configured, but we already had sudo set up. We installed four hardening packages (UFW, OpenSSH server, fail2ban, unattended-upgrades) plus neovim before touching group membership.
2.2 Create a non-sudo daily-driver user
useris the non-privileged day-to-day account, separate from the admin account that has sudo.
# During user creation the password will be min 15 characters in accordance to the NIST.SP.800-63B-4 (2026) Policy implemented in Linux
adduser user # creates user, sets password interactivelySeparates daily activity from administration: this account cannot escalate to root. Your existing admin account (the one in sudo group) is used only for management.
If you have not already done so, also generate an Ed25519 key for your admin user on your workstation (
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/debian_admin) and copy it into that admin user’s~/.ssh/authorized_keysbefore disabling password auth in §2.4.
2.3 UFW — deny incoming, allow only SSH
ufw default deny incoming
ufw default allow outgoing
ufw limit 22/tcp comment 'SSH (rate-limited)'
ufw logging low
ufw --force enable
ufw status verboseSets a deny-by-default inbound policy, allows all outbound, and rate-limits SSH (UFW drops connections after 6 attempts in 30 seconds — a quick brute-force speed bump on top of fail2ban). ufw status verbose should show Default: deny (incoming), allow (outgoing).
2.4 Harden /etc/ssh/sshd_config
# Remove non-ed25519 host keys
rm -f /etc/ssh/ssh_host_rsa_key* /etc/ssh/ssh_host_ecdsa_key* /etc/ssh/ssh_host_dsa_key*
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" -a 100 2>/dev/null || true
# Re-write the sshd_config
Port 22
AddressFamily inet
HostKey /etc/ssh/ssh_host_ed25519_key
PermitRootLogin no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication no
UsePAM yes
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PrintMotd no
LogLevel VERBOSE
KexAlgorithms curve25519-sha256,[email protected]
Ciphers [email protected],[email protected]
MACs [email protected],[email protected]
# restart ssh
sshd -t && systemctl reload sshEdit the AllowUsers line to the exact username of your sudo admin account before reloading (debian). sshd -t validates the config — never reload without that check. After reload: open a new terminal and confirm you can still log in before disconnecting the current session.
2.5 fail2ban — throttle SSH brute force
cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
port = 22
backend = systemd
maxretry = 3
findtime = 10m
bantime = 1h
EOF
systemctl enable --now fail2ban
fail2ban-client status sshdBans any IP that fails SSH auth three times within ten minutes for one hour. Pairs with UFW’s limit for layered defense.
2.6 unattended-upgrades — auto-apply security patches
dpkg-reconfigure -f noninteractive unattended-upgrades # enables daily timer
cat > /etc/apt/apt.conf.d/51hardening <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
EOF
systemctl restart unattended-upgradesDebian security updates apply automatically each day. Reboot stays manual so the box never restarts unexpectedly during a stream / recording.
2.7 Lock the root account
Before locking root, prove the admin user has a working sudo path.. Open a fresh SSH session as the admin user, run the check below, and only then lock root. If
sudo -vfails, do not proceed — fix the group membership first, otherwise the box becomes unmanageable.
# --- in a NEW terminal, as the admin user (via SSH key) ---
whoami # should be 'debian'
groups # 'sudo' must appear
sudo -v # prompts for admin's password; exits 0 on success
sudo whoami # should print 'root'
# --- back in the existing root session ---
passwd -l root # lock the root password
passwd -S root # status field should now be 'L' (locked)Locks the root password — root can no longer log in directly (SSH already forbids it). Administration is now strictly via sudo from the admin account.
2.8 Debian — final verification
ufw status verbose # default deny in, allow out, 22/tcp LIMIT
ss -tlnp # only sshd should be listening on a public IP
fail2ban-client status sshd
systemctl list-timers apt-daily* # security upgrade timers active
passwd -S root # status field should be 'L' (locked)
# from your OpenWRT
ssh [email protected] # refused without the pubkey authEvery line should match the expected result before you call the box hardened.
Peace!