SSH Across Starlink CGNAT and a Netgate 2100 DMZ

This design explicitly assumes Starlink’s default IPv4 CGNAT, a Netgate 2100, and its LAN4 DMZ at 192.168.60.0/24. The Raspberry Pi 5 is 192.168.60.10. Nginx listens on loopback, cloudflared publishes public web traffic only, and Tailscale carries private administration. There is no WAN SSH forward and there is no SSH through the Cloudflare public web hostname.

CGNAT changes the path, not the authorization model. An inbound Starlink IPv4 connection cannot be treated as a usable route to the Pi. Keep SSH on the private Tailscale interface and retain a narrow trusted-LAN break-glass path; do not work around CGNAT by exposing TCP/22.

Draw the intended paths

PathAllowed purposeBoundary
Internet → Cloudflare edge → cloudflaredPublic web requests to the approved hostname.Nginx is loopback-only; this is not an SSH path.
Pi → Starlink/InternetStateful outbound Cloudflare, Tailscale, DNS, NTP, and updates.LAN4 egress rules are narrow and logged.
Administrator → Tailscale → PiPrivate SSH TCP/22 for named administrators.Tailnet grants or ACLs plus the Pi’s tailscale0 host rule and SSH keys.
Trusted LAN → PiDocumented break-glass SSH only.One source or management group, key-only, logged, tested, and not a broad LAN trust.
WAN → Pi or NetgateNothing unsolicited.No WAN rule or port forward for SSH, Tailscale, Cloudflare Tunnel, or Nginx.

Netgate firewall rules are evaluated on the interface where a packet enters. A rule on LAN4 therefore describes traffic entering LAN4 from the DMZ Pi, not traffic arriving from the WAN and not trusted-LAN traffic. Stateful replies to approved Pi egress are allowed by the existing state; that does not create an inbound WAN pass.

Prepare the Netgate 2100 LAN4 policy

  1. Export a known-good Netgate configuration and keep local console access. Perform the switch-port and firewall work from the trusted LAN, never while connected through LAN4.
  2. Verify LAN4 is an isolated, untagged access network with gateway 192.168.60.1/24, the Pi has 192.168.60.10/24, and no other route bridges the DMZ to trusted, storage, management, or firewall control-plane networks.
  3. On Firewall > Rules > LAN4, block and log DMZ-to-private destinations first. Include every actual trusted/LAN/VLAN subnet, Netgate interface address, management, storage, backup, and RFC1918 ranges. Keep IPv6 disabled on this lab or create equivalent IPv6 policy before advertising it.
  4. Allow and log only the Pi’s documented outbound DNS, NTP, updates, Cloudflare Tunnel transport, and Tailscale control/relay/transport. The current Tailscale behavior commonly uses TCP/443, UDP/3478, and UDP/41641; verify the installed version’s documentation.
  5. Finish with a logged reject or the carefully reviewed default deny. Do not use “LAN4 to any” as a shortcut, and do not add a WAN rule for TCP/22 or UDP/41641.

Keep cloud service endpoint address lists current from official documentation rather than guessing CDN addresses. Rule counters and logs must identify whether traffic was permitted for Cloudflare, Tailscale, DNS, or updates. A successful web request should correlate with an outbound connector flow, never with an inbound WAN rule.

Keep the web and administration planes separate

Validate that Nginx binds only to 127.0.0.1:8080 and that cloudflared maps only the approved public web hostname to that loopback origin. Do not put ssh:// or TCP/22 in the tunnel configuration. Do not point an SSH client at the Cloudflare web hostname: a browser-oriented public hostname is not an SSH authorization boundary. Test that direct access to 192.168.60.10:8080 from an outside network fails and that the public hostname serves only the intended web application.

ss -lntp
curl --fail --silent --show-error http://127.0.0.1:8080/ >/dev/null
cloudflared --version
tailscale status
ip route

Use the installed package’s service and diagnostic commands; do not paste tunnel tokens or Tailscale auth keys into logs.

Carry SSH over Tailscale only

Enroll the Pi and administrator devices in the intended tailnet, require MFA for the identity provider, remove stale devices, and choose a Tailscale grants or ACL policy that names the administrator group, the Pi, and TCP/22. A policy example must be adapted to the tailnet’s current syntax and identity names:

{
  "grants": [
    {
      "src": ["group:ssh-admins"],
      "dst": ["tag:pi-dmz"],
      "ip": ["tcp:22"]
    }
  ]
}

Legacy ACL syntax and grants differ by tailnet policy version; validate in the Tailscale policy editor and preserve the previous policy for rollback. The Pi still needs ordinary SSH authorization: key or certificate, allowed principal, host key verification, and account policy.

On the Pi’s existing nftables inet input chain, preserve loopback and established/related rules, then add the narrow overlay rule:

ct state established,related accept
iifname "tailscale0" tcp dport 22 accept

Do not replace this with iifname "tailscale0" accept. That would permit unrelated services and protocols. If the trusted-LAN break-glass source is a known management host, add a separate source-address-and-TCP/22 rule, not a blanket trusted-LAN accept.

Understand direct transport and DERP

UDP/41641 can help a Tailscale peer establish a direct encrypted WireGuard path, but Starlink CGNAT may prevent unsolicited inbound reachability. It is optional and not an SSH port. If the Pi’s LAN4-facing device is really eth0, a site may permit:

iifname "eth0" udp dport 41641 accept

Use it only after reviewing the LAN4 and host threat model. Keep SSH restricted to tailscale0 (plus the separately documented break-glass source). Tailscale can fall back to an encrypted DERP relay over TCP/443; test both a direct result and a deliberate UDP-blocked relay result. Do not weaken authorization because DERP is slower, and do not create a WAN port forward for UDP/41641.

Test matrix and rollback

Test sourceExpected resultEvidence
Approved tailnet administratorSSH TCP/22 to Pi succeeds with the expected key.Tailscale policy, host rule, and sshd success log.
Unauthorized tailnet identityTCP/22 denied.Grant/ACL decision and no successful sshd login.
Outside IPv4 networkNo WAN SSH route; public web only at the approved hostname.Netgate WAN rules, external scan/test, Cloudflare request log.
Trusted-LAN break-glass hostOnly the named host can SSH when Tailscale is stopped.Firewall counter, sshd log, and negative test from another LAN host.
Pi egress to private destinationsBlocked and logged on LAN4.Netgate rule counter and log.
Direct and UDP-blocked Tailscale pathDirect where possible; encrypted DERP fallback when blocked.tailscale netcheck/tailscale ping, counters, and latency record.

Back up Netgate configuration, nftables/SSH drop-ins, Tailscale policy, cloudflared configuration, and recovery notes. Roll back in reverse order: remove temporary transport or grant changes, restore the prior host/firewall policy, restore the prior Tailscale policy, then restore the Netgate configuration through trusted LAN or console. Re-test the no-WAN-forward and public-web-only assertions after every rollback.

Build the host procedure with Operate and Recover SSH on the Raspberry Pi 5. The network baseline is Reference Architecture: Starlink, Netgate 2100, and Raspberry Pi 5.

Official references