Test the Complete Homelab from Outside

A successful browser load is not a complete security test. Test this homelab from networks that are genuinely outside it: Starlink’s default IPv4 CGNAT remains between the home router and the internet, the Netgate 2100 isolates LAN4 (192.168.60.0/24), the Pi 5 is 192.168.60.10, Nginx is only 127.0.0.1:8080, and cloudflared publishes the intended web hostname. Tailscale is the separate private administration path.

Test from outside, not from a hairpin. A phone on home Wi-Fi, a NAT reflection success, or a cached DNS answer cannot prove public reachability or isolation. Use cellular data, an independent fixed connection, and—where applicable—a separate IPv6-capable network.

Record the expected design

Public hostname:       www.example.net
Public path:           Cloudflare DNS/proxy -> named tunnel -> 127.0.0.1:8080
DMZ host:              192.168.60.10
Private administration: Tailscale only
WAN port forwards:     none
Public SSH:            none
IPv4 expectation:      Public web works through the tunnel, not the Starlink address
IPv6 expectation:      No public path unless an intentional, secured path is documented

Capture the baseline Cloudflare DNS records, certificate name, Netgate rule counters, tunnel status, Tailscale device status, and synchronized test time. Use a test account that is not an administrator and do not place real credentials in command history or screenshots. For every run, resolve both records and force both address families; a resolver preference or an application fallback must not hide a broken path:

dig +short A www.example.net
dig +short AAAA www.example.net
curl -4 -vI --fail https://www.example.net/
curl -6 -vI --fail https://www.example.net/

Record the selected address, TLS certificate subject/SAN, TLS verification result, HTTP status, redirect location, and response headers for both curl runs. Repeat with a full-body request when the application has meaningful content; -I alone does not prove the response body path.

External test matrix

Test source and actionExpected resultEvidence to collect
IPv4-only cellular: resolve and request the public HTTPS hostnameExpected certificate and application response through CloudflareDNS answer/chain, HTTP status, TLS name, Cloudflare and Nginx logs
IPv6-capable external network: query A and AAAA, then force curl -4 and curl -6Each family matches the documented design; AAAA is absent when IPv6 is disabled, or reaches only the intentionally secured pathBoth DNS answers, selected addresses, certificates, HTTP statuses, and Netgate IPv6 logs
Independent resolver: query authoritative and public recursive DNSOnly the intended record and tunnel target appear; no stale residential A/AAAA or wildcard surpriseAnswers, TTL, CNAME chain, DNS change audit
HTTPS with wrong Host/SNI and an unrecognized hostnameReject, default catch-all error, or intended Cloudflare error; never the applicationResponse status and tunnel/Nginx ingress evidence
Direct Starlink-observed/old WAN address, TCP 80/443/7844/8080/22No service is reachable by direct address; no WAN forward existsScanner results from an authorized test source and Netgate counters
Private administration from outside with Tailscale upOnly the named Pi ports for the administrator policy workTailscale path, policy decision, SSH/system log
Private administration without Tailscale or as an unauthorized identitySSH and administration fail; public website behavior is unchangedClient error and denied tailnet/host evidence
Attempt to reach Netgate WebGUI, DMZ origin, trusted LAN, and other DMZ hostsAll are unreachable from the public test networkTimeout/refusal and any expected firewall log
Authorized IPv6 test to each published Netgate and Pi IPv6 address: TCP 22, 80, 443, and 8080 as applicableOnly the explicitly documented public service works; Netgate management, SSH, the origin, and unused ports are closedIPv6 connection results, WAN rule counters/logs, Pi nftables/service logs, and the tested address inventory
Access-protected hostname with allowed and denied identitiesAllowed identity passes the policy; denied and unauthenticated users do notAccess event, application response, and no trusted-header bypass

Interpret timeouts carefully: they may indicate a filtered port, a missing route, or a test-network limitation. Repeat from at least two independent external networks and record the protocol and address family. For explicit owned-address exposure tests, use a narrow command such as nc -6 -vz <netgate-ipv6> 22 80 443 and the equivalent Pi address/ports, or an approved IPv6 scanner; never scan networks or hosts you do not own or have permission to test.

Prove the disabled-IPv6 posture

If IPv6 is disabled for LAN4, the acceptance state is stronger than “the browser did not use it.” On the Pi, verify that ip -6 addr shows no global (scope global) or ULA (fc00::/7) address, ip -6 route has no default route, and router advertisements have not supplied a route or prefix. Check the Netgate LAN4 status and IPv6 neighbor/router-advertisement state as well. From independent recursive resolvers and the authoritative DNS service, verify that the public hostname has no AAAA record. Do not accept a hidden temporary address, a ULA-only management path, or a stale AAAA as “disabled.” If IPv6 is enabled instead, inventory the exact stable addresses, WAN/LAN4 pass rules, host firewall rules, and AAAA records and run the explicit Netgate/Pi IPv6 exposure tests above.

Run failure tests and verify fail closed

  1. Stop cloudflared: the public hostname should fail with a tunnel/origin error, while the Pi’s private administration path remains available. No new inbound firewall rule should be needed.
  2. Stop Nginx or the application: the connector may remain healthy but the public request must not reveal an unrelated service or management interface. Restore the service and confirm normal logs.
  3. Block the documented tunnel egress temporarily: public access should fail, and the Netgate rule log should identify the block. Do not “fix” this by allowing all inbound traffic.
  4. Disable Tailscale on the Pi: private administration should fail while the public web route continues only if Nginx and the connector are healthy. Re-enable it and verify the device policy.
  5. Remove or stage an incorrect AAAA record in a controlled test: confirm clients do not acquire an unprotected IPv6 route. Restore the intended DNS state and allow recursive TTLs to expire before judging the result.
  6. Disable the LAN4 IPv6 path, when that is the documented posture: verify no Pi global/ULA address, RA, or default route remains and that external curl -6 fails because there is no AAAA/path, while forced curl -4 retains its intended result.
  7. Reboot the Pi and Netgate during a maintenance window: verify the Pi address, Nginx bind, connector, Tailscale service, rule ordering, and logs recover without manual exposure changes.

Check observability and cleanup

  • Correlate the test timestamp across Cloudflare events, Nginx access/error logs, Pi service journals, Tailscale events, DNS logs, and Netgate egress or deny logs.
  • Confirm the successful public request used the tunnel’s outbound path, not an unnoticed WAN NAT rule. Review NAT and WAN rules after every test and remove temporary rules immediately.
  • Check for leaked origin addresses, server headers, debug traces, directory listings, tokens, private URLs, and application errors in the response.
  • Record test source, IPv4/IPv6 capability, DNS resolver, time, result, expected result, and remediation owner. Retest after DNS, certificate, firewall, connector, or application changes.

Acceptance criteria

Accept the deployment only when the intended public HTTPS service works from IPv4 and from the documented IPv6 posture, DNS has no stale A or AAAA exposure, Cloudflare Access behaves differently for allowed and denied identities, private administration requires Tailscale and least privilege, no public SSH or WAN forward exists, DMZ-to-LAN attempts are blocked and logged, and each induced failure recovers without broadening the policy. If any criterion is unknown, mark the service not ready and keep the public hostname disabled.

Sequence navigation

Previous: Logging Updates Backups and Recovery · Return to the access-method overview

References