Trust Chapter 12 65 min
Reachable from anywhere
Give enrolled family devices private HTTPS access without putting photo and video traffic through an unsuitable public tunnel.
By the end of this chapter you should be able to
- Compare a public listener, a TLS-terminating tunnel and a private mesh path
- Give family identities access to the photo service and nothing administrative
- Publish the loopback-only Immich backend through Tailscale Serve
- Verify the service from mobile data and verify that non-members cannot reach it
Your administrator account can reach the server remotely over SSH. The rest of the household needs the photo application, not a shell and not monitoring ports.
Choose the failure and trust boundary #
A router port-forward sends unsolicited internet traffic to the server. It can work, but the origin address and listener are public and the exposed software must withstand continuous scans and urgent vulnerabilities.
An HTTP tunnel has the server dial a provider. The provider terminates browser TLS, reads the requests and responses, then carries them over an encrypted tunnel to the connector. There is no inbound router rule, but the provider becomes part of the confidentiality and availability boundary.
A private mesh service is reachable only by enrolled identities and devices. It adds client installation and account recovery work, but it keeps the application off the public internet and preserves end-to-end WireGuard encryption to the home server.
This chapter chooses the third path for photographs and video.
Do not route Immich through an ordinary Cloudflare plan #
Cloudflare's current Tunnel routing documentation says Free, Pro and Business customers must use an appropriate paid service for video and other large files. Its service-specific terms also allow image or video use that is disproportionate to other traffic to be limited or disabled.
The familiar 100 MB upload limit is not the whole constraint. A smaller photo or video can still be media delivery covered by the service terms. Do not present an ordinary Cloudflare public hostname as a free Immich transport.
Valid alternatives are:
- the private mesh used below;
- a private Cloudflare network route whose clients run the required client;
- a self-hosted relay or VPS whose bandwidth and terms cover the workload;
- a provider product whose current contract explicitly covers the media traffic.
A TLS-terminating tunnel can still suit a small public web application after reviewing its trust and service terms. This book keeps both Immich and the chapter 13 identity service on the private mesh.
Give the family one application grant #
Invite each family member to the tailnet and enrol the devices that need Immich. Extend chapter 10's policy deliberately:
{
"groups": {
"group:admins": ["you@example.com"],
"group:family": ["you@example.com", "<family-member@example.com>"]
},
"tagOwners": {
"tag:server": ["group:admins"]
},
"grants": [
{
"src": ["group:admins"],
"dst": ["tag:server"],
"ip": ["tcp:22"]
},
{
"src": ["group:family"],
"dst": ["tag:server"],
"ip": ["tcp:443"]
}
]
}
Replace the angle-bracket address before saving. The family grant does not include SSH, Grafana, Prometheus, databases or arbitrary ports. Test one family identity against TCP 443 and TCP 22 in the policy preview; require the first to pass and the second to fail.
Make Immich loopback-only again #
Tailscale Serve proxies only to a local backend. Add this port to immich-server; it does not replace the LAN Traefik route from chapter 8:
immich-server:
ports:
- "127.0.0.1:2283:2283"
Make the change in the canonical laptop repository, commit it, and repeat chapter 5's rsync deployment before running the server commands.
$ sudo docker compose -f /home/admin/homeserver/immich/compose.yaml up -d immich-server
$ ss -tln | grep ':2283 '
Require the listener to show 127.0.0.1:2283, never 0.0.0.0 or [::]. A second LAN device must still fail to reach http://192.168.1.20:2283.
Publish the local backend inside the tailnet #
Tailscale Serve creates a private HTTPS endpoint for a service on the same device. Enable MagicDNS and HTTPS certificates in the Tailscale DNS settings, after accepting that the machine's full *.ts.net name will appear in public Certificate Transparency logs.
Then proxy the loopback backend in the background:
$ sudo tailscale serve --bg 127.0.0.1:2283
$ sudo tailscale serve status
Use the exact https://<machine>.<tailnet>.ts.net URL printed by status as the remote Immich server URL. Tailscale terminates HTTPS on the server and renews the certificate for Serve; there is no certificate file for Traefik to reload and no 90-day timer to invent.
Serve removes spoofed Tailscale identity headers before adding its own. Immich still performs its own authentication; do not treat a proxy header as an application account.
Test from a different network and identity #
On an enrolled phone, disable Wi-Fi and load the exact Serve URL over mobile data. Upload one test photograph and one short test video, then download both. Confirm the server-side originals appear under the upload root and that the next backup succeeds.
Run three negative checks:
- A family identity cannot open SSH according to the policy tester.
- A signed-out or unenrolled device cannot load the Serve URL.
- The router has no manual NAT, UPnP or PCP mapping for ports 80, 443 or 2283; test the home's public IPv4 and any global IPv6 from outside.
https://example.com:9090 would test DNS or some external edge, not the home origin. Inspect the actual router and addresses instead.
The Serve URL works for me but not a family member
Separate enrolment, policy and application-login failures
Paste this into a new agent session. It carries everything the agent needs to know about where you are, and asks it to walk you through the problem rather than fix it for you.
group:family, TCP 443 is granted, or Immich itself is rejecting the login.Immich is reachable on port 2283 from the LAN
Find why the loopback publication widened
Paste this into a new agent session. It carries everything the agent needs to know about where you are, and asks it to walk you through the problem rather than fix it for you.
192.168.1.20:2283. I expected only 127.0.0.1:2283. Help me inspect the rendered Compose ports and Docker mappings before changing the firewall.Keep the public boundary explicit #
The photo application is available away from home, but it is not public. Every user needs an enrolled device and a permitted tailnet identity. That client requirement is the chosen cost.
The next chapter adds central application identity through a distinct private Tailscale Service. It does not widen the Immich or administration grants.
Done when
- Every family device that needs Immich is enrolled and recoverable
-
group:familyhas TCP 443 to the tagged server and no SSH grant - Immich publishes only
127.0.0.1:2283for the Serve backend -
tailscale serve statusshows the expected private HTTPS URL and local target - A test photo and video work over mobile data on an enrolled device
- An unenrolled device cannot load the URL
- The router and globally routed IPv6 expose no origin listener
- No ordinary Cloudflare public-hostname route carries Immich media
- You can state the Certificate Transparency and Tailscale account-recovery costs
What you picked up
- A public port, a TLS-terminating tunnel and a private mesh have different trust boundaries.
- Ordinary Cloudflare Free, Pro and Business tunnel routes are not the book's media-delivery path.
- Tailscale Serve gives enrolled devices private HTTPS to a loopback backend.
- Grant the family only the application port; keep SSH and future dashboards administrative.
- Test a real upload and the negative paths from outside the home.