Introduction
I thought that getting Plex Media Server working with Tailscale would be as easy as getting Tailscale to work with Kibana. I have a NAS/container-workload server, and I use it to run various services like ELK or GIS servers. After years of streaming our favorite movies from Amazon Prime, and putting up with degraded quality when everyone in the neighborhood is home doing the same thing, I figured it’s time to set up Plex. How hard can it be? Turns out it was a pain! I originally though the problem was due to how I run containers.
I’ve been meaning to write about my new NAS server but haven’t had time. It’s based on a SuperMicro EPYC SOC CPU, and workloads are LXD containers managed by an elegant sub 300 line bash script called Orca. Each workload runs it’s own Tailscale client. Tailscale in LXD supposedly requires access to a tun device which I have not done.
Installing Plex
The NAS server runs NixOS, but the workloads use Ubuntu so I followed the standard Debian instructions to install Plex. It’s super easy to configure reproducible workloads with mapped storage using Orca.
Plex Media Server is a 19MB ELF file written in C++. When the service starts, it’s listening on 0.0.0.0:32400
. Plex really wants you to create an online account that you can use to log into your server. Using an online account hides a lot of your server settings, gives you a STUN to stream your content from anywhere outside your local network, and lets you buy media from Plex.
Lots of people do not want to use Plex’s online accounts. Your choices are online accounts or no authentication at all.
Disabling Plex authentication for local networks
I don’t care about auth for my movies and TV shows, but my threat model does include malicious JS running in an older web browser making HTTP calls to 19MB of C++. That would require an old browser or a new browser vulnerability, but still…
There are advanced configuration settings in Preferences.xml
.
allowedNetworks
let’s you define, a list of networks that are allowed to access PMS without authentication. Must be listed with full subnet, e.g. 192.168.1.0/255.255.255.0,10.10.10.0/255.255.255.0
.
I want to restrict access to Plex by only exposing it on a Tailnet. Many media streaming devices support Tailscale. The problem is that Tailnet IPv4 addresses are on the Carrier Grade NAT IP space 100.64.0.0 - 100.127.255.255
and Plex will only disable auth for addresses on the RFC 1918 Private Network space 10.0.0.0 - 10.255.255.255, 172.16.0.0 - 172.31.255.255, 192.168.0.0 - 192.168.255.255
.
If you configure allowedNetworks
with an address range not in RFC 1918, Plex will ignore it. We can overcome this but it takes a little work. It’s not clear to me whether Plex looks at the packet’s SRC address or not, but it does look at several HTTP headers for each request to determine the source IP for the request.
Setting up a reverse proxy with Caddy
Caddy is a friendly and modern HTTP server written in Go. You can set up a reverse proxy in a few lines of code, and it integrates with Tailscale’s experimental TLS, so you don’t have to provide it with certificates.
You install Caddy on Ubuntu in the normal way. They provide a unit file and instructions for making it persistent. The Tailscale daemon can be configured to give the Caddy user access to the TLS certs.
Now we just need to rewrite some headers to replace the Tailscale CGNAT IP address with a private space IP. You can use any private space IP you want so long as it’s the same in the Caddyfile and the allowedNetworks
attribute in Preferences.xml
.
Here is my Caddyfile:
orca-plex.tailnet-xxxx.ts.net {
reverse_proxy localhost:32400 {
header_up -Referer
header_up -X-Forwarded-For
header_up Origin "orca-plex.tailnet-xxxx.ts.net" "10.54.0.88"
header_up Host "orca-plex.tailnet-xxxx.ts.net" "10.54.0.88"
header_down Location "10.54.0.88" "orca-plex.tailnet-xxxx.ts.net"
}
}
The reverse_proxy directive lets you remove and modify upstream and downstream request headers.
- Remove the
Referer
header since it has the Tailscale host and is not needed. - Remove
X-Forwarded-For
which is added byreverse_proxy
but also has our source IP/host and is not needed. - Replace the Tailscale host in the
Host
header with our private IP. - Replace the private IP with the Tailscale host in the
Location
header on the response.
Final thoughts
It’s an interesting choice by Tailscale to use the CGNAT address space. They have a bit to say on it here. I think lot of people would have problems using Tailscale with Plex, but when googling, I didn’t find anyone mentioning it. This is a bit troubling since I may be missing something very simple and do not need the reverse proxy at all.
Be careful which headers you remove or manipulate. Originally I was removing the Origin
header but that wipes out CORS protection. Swapping the origin header when it matches our Tailnet address should be safe.