or how to be okay with not proselytizing Tailscale
I regularly find myself wanting to share photos from my self-hosted Immich instance on my server with my friends.
If you’ve followed my previous post on my self-hosting journey, you’ll know I love Tailscale and its ability to let me share things with friends and family without exposing my local network. However, Tailscale obviously isn’t the best when my friends and family don’t want to install it and simply want a photo album detailing my most recent trip.
Fine.
I’ll do something about it.
What if we use Cloudflare Tunnels with custom JWT token authentication and split access to important albums and not so important albums?
Am I overengineering this? :)
The problem
My Immich instance is behind Tailscale. Many of my friends are not on Tailscale.
I want to be able to send a short link to my friends with one (1) album behind it with expiry and some sort of authentication.
I want to be able to send a short link to anyone with one (1) album behind it with expiry and no authentication.
These are two similar but different problems.
Immich Public Proxy
The great thing about the internet is that sometimes you think of a problem and someone’s already made a solution for it.
I have an album on Immich I want to share with people. I don’t want them to be able to view anything else, and I don’t want them to be able to modify the album. I set up Immich Public Proxy (IPP) on top of my Immich instance to intercept requests. Only shared albums are accessible.
Perfect, I have read-only access to albums utilizing native share links from Immich itself.
But how do I give my friends access to this?
Cloudflare Tunnel/Access
I haven’t really used Cloudflare Tunnels until now. Cloudflare Tunnels are a better version of Tailscale Funnel (makes sense considering they’re in beta) and work similarly in principle.
Cloudflare creates a proxy between your local service and the internet through your domain without exposing your actual network.
The main draw behind Cloudflare here is that while it bridges the gap between the internet and your service, it also gives you access to Cloudflare Access – an access policy that applies to the entire domain.
This means that, for me, I can
a) Serve my local infrastructure to the broader Internet and
b) Limit who can access the service through email control lists
Cloudflare Access sits in front of the entire domain, so upon visiting my domain, you’re greeted with a Cloudflare login splash screen and only authorized emails will even get the one time passcode.
You should see a Cloudflare Login screen unless I added your email to the whitelist ;)
I want truly public share links
Wow this is great! I’m done, right?
Well, I wanted to be able to create a version of this without identity-based authentication.
For example, what if I have an album I don’t care getting out on the internet, but I want super easy access for people without one time passcodes and email whitelists?
What if we run our own JWT token generation + shortlink service?
imshare, a custom JWT authentication webserver
With lots of Claude steering, I built imshare, a webserver that takes an Immich album id, a TTL, and a label, and generates a presigned url with a token. imshare consists of two parts– the generator, imshare, and the verifier, imshare-verify.
The generator is a CLI + lightweight api/htmx server that runs on my Tailnet. This is not publicly accessible at all. imshare generates the url with a token and keeps track of both the shared link, its short version, its expiry, etc.
imshare-verify sits between IPP and the tunnel and intercepts all requests made to IPP. A proxy for a proxy, if you will. It takes the token and compares the HMAC signature against the database. It also checks if the entry has been revoked. If it matches and is not revoked, traffic is proxied correctly to IPP. If it doesn’t match or is revoked, the server never forwards traffic to IPP, and my photos are safe.
This link is truly public and can be shared wherever. But the TTL ensures the links can expire, and the label ensures the links are trackable.
The public only sees the link with a token until the token expires or is revoked.
A sqlite database keeps track of the links and I have an internal HTMX dashboard to revoke, copy, and view QR codes for each of the links.
I was even able to create a small shortlink generator so that instead of sending massive links with album ids + tokens, I could send a small link like https://pub.nith.sh/s/1Yz5YA
My tailnet is accessible behind my sumoftheir.parts domain while the public domain IPP is accessible only through pub.nith.sh (or img.nith.sh for the Cloudflare access version).
This way, only IPP is ever accessible to the public.
Great, huh?
Previous image with only domain changed | This should return ‘Missing token or session cookie’
Previous image with shareable link generated | This should return some pasta I made recently :)
But how does this work in practice? How do we generate links and share them easily?
My workflow
I have imshare running on the same LXC container as my Immich instance itself on my Proxmox host.
Because the container is on my Tailnet, I can access https://photos.sumoftheir.parts/imshare/ from any of my devices. This runs the HTMX webpage where I can create new links and view existing links. This is great for when I’m at the computer.
However, for my phone, where I normally send photos from, I can simply create a shareable link from the Immich app, copy the link and start my iOS Shortcut that calls the https://photos.sumoftheir.parts/imshare/api/generate endpoint with the album id, TTL and photo name, and automatically copies the result of the API call (short_url) to my clipboard.
This public link is immediately ready to send to my friends. This makes it super seamless to share any of my Immich links with friends.
What’s even more cool is that since all my photos from my phone are also backed up to Immich, I can send photos that I just took immediately over if I wanted.
And since the link is publicly accessible, it even loads a little preview for them!
Conclusions
This is pretty neat, in my opinion.
I’m not super sure the security implications of running my own JWT token authentication infrastructure, but my risk exposure is quite low here.
The worst thing that could happen is if Immich public proxy is insecure or if my tokens are incorrect. The outcome would be shared albums would just continue to be shared.
However, IPP relies on the underlying Immich share system anyway. So, revoking a share link there should be good enough to prevent any misuse.
I think Cloudflare tunnels are pretty great. I wish Tailscale had a similar alternative with Cloudflare access-like policy lists. Tailscale ACLs seem like they could do that, but the feature just isn’t there yet.
Until then, I’ll continue to use imshare and IPP as an easy way to share one-off trip albums or photos with friends and family while still retaining all the benefits of self-hosting.
If it’s one thing I’ve learned, getting people to install Tailscale is harder than implementing these sorts of solutions ;)