Getting Plex Media Server to work with Tailscale

Posted: Friday, Jul 1, 2022

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.

  1. Remove the Referer header since it has the Tailscale host and is not needed.
  2. Remove X-Forwarded-For which is added by reverse_proxy but also has our source IP/host and is not needed.
  3. Replace the Tailscale host in the Host header with our private IP.
  4. 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.