Prepare Raspberry Pi 5 as a DMZ Web Server

This procedure prepares a Raspberry Pi 5 for the reference design: an IPv4-only host at 192.168.60.10 on the isolated Netgate LAN4 DMZ. It installs the minimum base tools, applies a fixed address, enables a host firewall with a small egress policy, and creates a safe placeholder for the local Nginx origin. The next article installs and configures Nginx; this article does not publish anything to the internet.

Do not connect this host to a trusted LAN during the build. Use a dedicated LAN4 cable after the Netgate DMZ is ready. There is no WAN port forward, no pfSense management access from the Pi, and no assumption that a “DMZ” label makes the host safe.

Prerequisites

  • Complete Reference Architecture: Starlink, Netgate 2100, and Raspberry Pi 5.
  • Export the Netgate configuration and verify console recovery before modifying LAN4. LAN4 must provide 192.168.60.1/24 by DHCP or a documented static configuration.
  • Use a supported 64-bit Raspberry Pi OS or Debian Bookworm image, current firmware, a reliable power supply, and encrypted or otherwise protected storage where the application’s data requires it.
  • Have a trusted management host ready, but do not permit the Pi to reach pfSense WebConfigurator, SSH, trusted clients, storage, backups, or hypervisors.

Verify the OS and hardware before changing networking

Log in locally or through a temporary, trusted management path. Run the following and compare the release with the current Raspberry Pi and Debian support documentation:

cat /etc/os-release
uname -m
uname -r
vcgencmd get_throttled 2>/dev/null || true
ip -br link
ip -br address
ip route

The expected architecture is normally aarch64 for a 64-bit image. Do not infer that a command is suitable merely because it works on another Pi; confirm the release, interface name, boot method, and package source. Identify the wired interface from ip -br link; the examples below use eth0, but predictable names can differ.

Patch the base system and install only required tools

sudo apt update
sudo apt full-upgrade
sudo apt install --no-install-recommends ca-certificates curl nftables openssh-server unattended-upgrades
sudo reboot

After the reboot, repeat the version checks. Schedule maintenance rather than assuming unattended upgrades can safely restart an application. Keep the package repository configuration backed up or recorded; do not add an untrusted repository to obtain a newer-looking package.

Assign the fixed DMZ address

Use one network manager, not several competing managers. Raspberry Pi OS may use NetworkManager on newer images; a Debian installation may use systemd-networkd, ifupdown, or another configured service. Inspect the active path first:

systemctl is-active NetworkManager systemd-networkd networking 2>/dev/null || true
nmcli device status 2>/dev/null || true
networkctl status 2>/dev/null || true
ip route

If NetworkManager is active, a connection profile can be created or edited as follows. Replace Wired connection 1 with the actual profile and verify the interface before applying:

nmcli connection show
sudo nmcli connection modify "Wired connection 1" \
  ipv4.method manual ipv4.addresses 192.168.60.10/24 \
  ipv4.gateway 192.168.60.1 ipv4.dns "192.168.60.1" \
  ipv6.method disabled
sudo nmcli connection up "Wired connection 1"

If the router provides DNS on another approved address, use that documented resolver instead. If systemd-networkd or another manager owns the interface, follow that manager’s current Bookworm documentation instead of applying the NetworkManager commands. Do not run both configurations. A static host address is not a DHCP reservation; ensure no other device uses 192.168.60.10.

Verify the boundary before installing a service

ip -4 address show dev eth0
ip -4 route
getent hosts deb.debian.org
ping -c 2 192.168.60.1
curl -4I --max-time 10 https://deb.debian.org/
curl -4 --max-time 5 http://192.168.60.1/

The gateway and package repository test should work if the DMZ policy permits them. The final request should fail or be refused: that address is the firewall, not a management target. From a trusted client, separately verify that the Pi cannot reach the pfSense GUI or SSH, trusted LAN addresses, storage, or backup services. Review the Netgate rule counters and logs. If any forbidden path works, stop and correct the DMZ policy before proceeding.

Install a conservative host firewall

The firewall below is an additional host boundary. It allows established traffic, loopback, ICMPv4 for diagnostics, DHCP only if you still use DHCP during commissioning, SSH only from the chosen trusted management address, and outbound DNS, NTP, HTTP, and HTTPS. Adapt DNS, NTP, package, and management destinations to the Netgate rules. Do not copy a broad trusted subnet into the SSH rule without deciding who may administer the host.

sudo tee /etc/nftables.conf >/dev/null <<'EOF'
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    iifname "lo" accept
    ct state established,related accept
    ip protocol icmp accept
    # Remove this DHCP rule after the static address is confirmed.
    iifname "eth0" udp sport 67 udp dport 68 accept
    # Replace 192.168.50.20 with the approved trusted admin host.
    ip saddr 192.168.50.20 tcp dport 22 accept
  }
  chain forward {
    type filter hook forward priority filter; policy drop;
  }
  chain output {
    type filter hook output priority filter; policy drop;
    oifname "lo" accept
    ct state established,related accept
    ip protocol icmp accept
    udp dport 53 ip daddr 192.168.60.1 accept
    udp dport 123 ip daddr 192.168.60.1 accept
    tcp dport { 80, 443 } accept
  }
}
EOF
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftables
sudo nft list ruleset

The package and tunnel design may need documented DNS or egress exceptions. Add the smallest justified rule, validate it, and record why it exists. Never “fix” a failure with policy accept or an unrestricted DMZ net to any rule. Before enabling this policy remotely, keep a local console available and confirm the SSH source address; a wrong rule can lock out administration.

Harden administration and service identity

  • Create a named, non-root administrator and use sudo; disable direct root login and password SSH after verified key-based access.
  • Keep SSH reachable only from the private management path. The existing trusted-LAN source rule is the recovery path and must remain in place when Tailscale is added; do not publish SSH through Cloudflare’s public HTTP hostname.
  • Set a hostname that does not disclose a public role, set the correct time zone, and verify time synchronization. Do not put internal names or secrets in public headers, logs, or screenshots.
  • Store application data separately from operating-system backups where appropriate, define retention, and test restoration before calling the server production-ready.
sudo hostnamectl set-hostname pi-dmz-web
timedatectl status
sudo systemctl enable --now systemd-timesyncd 2>/dev/null || true
sudo passwd -l root
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication) '

Only disable password authentication after a second session has successfully authenticated with a key and the recovery method is documented.

Plan the later Tailscale SSH exception

Tailscale is a later, separately tested change—not a reason to broaden the DMZ. Before enabling it, approve the tailnet identity and ACL, record the admin node’s stable Tailscale IPv4 address, and preserve the trusted-LAN SSH rule above as an independent recovery path. Then add a narrow host-firewall accept rule on the Tailscale interface for SSH, for example:

# Add only after tailscale0 exists and the admin policy is verified.
iifname "tailscale0" tcp dport 22 accept

The interface-and-port rule is the required service rule for Tailscale SSH: it permits only TCP/22 arriving over tailscale0; tailnet grants/ACLs and SSH key authorization must still restrict the administrator. If the site requires host-level source filtering as well, add the approved administrator’s Tailscale address to this rule rather than accepting every tailnet address. The rule belongs in the nftables input chain and must be tested without removing the trusted-LAN recovery rule.

The control and relay path requires approved DNS and outbound TCP 443. UDP 3478 (STUN/NAT discovery) and outbound UDP 41641 are transport options for direct peer paths; they are not replacements for the tailscale0 SSH service rule. If this host is deliberately supporting direct Tailscale paths, add the optional inbound rule below and the matching outbound UDP allowances:

# Optional direct-path transport; add only after reviewing the current policy.
iifname "eth0" udp dport 41641 accept

# In the output chain, alongside approved DNS and TCP 443:
oifname "eth0" udp dport { 3478, 41641 } accept

The optional eth0 input rule exposes only the Tailscale WireGuard transport on UDP 41641; WireGuard cryptographically authenticates peer traffic before it can become a tailnet connection. It may improve direct paths, but it does not guarantee them behind CGNAT. Keep the encrypted DERP relay fallback over the approved TCP 443 path. Confirm current Tailscale port requirements before adding these rules, log each exception, and avoid an unrestricted outbound rule. A host firewall accept rule alone cannot make Tailscale work when the DMZ egress policy blocks its control or relay traffic.

Prepare the local origin directory

sudo install -d -o www-data -g www-data -m 0755 /srv/www/dmz-site
printf '%s\n' '<h1>DMZ origin is ready</h1>' | sudo tee /srv/www/dmz-site/index.html >/dev/null
sudo chown www-data:www-data /srv/www/dmz-site/index.html
sudo chmod 0644 /srv/www/dmz-site/index.html

This placeholder is intentionally local and is not yet served until Nginx is configured to bind loopback. Avoid uploading private keys, tunnel tokens, backups, or source-control directories beneath the web root.

Validation and rollback

  • Confirm 192.168.60.10/24, gateway 192.168.60.1, and the intended resolver; confirm there is no unexpected IPv6 address or default route.
  • From the Pi, verify only approved egress. From trusted and external networks, verify the Pi and firewall management services are not reachable.
  • Save the nftables file and active ruleset, then test a reboot during a maintenance window. Confirm the fixed address and firewall return.
  • If networking fails, use the local console to restore the previous manager profile and remove or correct the nftables change. If the Netgate boundary fails, restore its pre-change configuration from console or trusted LAN.

Continue with Configure Nginx on Raspberry Pi 5 only after these tests pass.

References