Skip to content

Instantly share code, notes, and snippets.

@doonga
Last active November 4, 2023 21:10
Show Gist options
  • Save doonga/dc91f2c52f17c9931e269966dfcb956d to your computer and use it in GitHub Desktop.
Save doonga/dc91f2c52f17c9931e269966dfcb956d to your computer and use it in GitHub Desktop.
Plex NGINX reverse proxy config using Cloudflare on Ubuntu 16.04
Credit where credit's due, I mainly used these 3 resources to get this all working. There were several others but I don't exactly remember which is which. So at least here are the 3 main places that I used.
The vast majority of this came from this link, the others helped me make it work for Plex.
http://emby.media/community/index.php?/topic/30975-reverse-proxy-with-ssl-hostname-routing-and-embyopenvpn-port-sharing/
https://gist.github.com/spikegrobstein/4384954
https://forums.plex.tv/discussion/207725/ubuntu-14-04-4-lts-plex-incorrectly-handles-lower-case-headers#latest
The data flow here is:
User -> Cloudflare -> Origin IP -> Haproxy -> NGINX -> Plex
All caching and acceleration stuff is turned off on Cloudflare. I'm using Full(Strict) with Authenticated Origin pulls and HSTS. I'm mainly using it to obscure my real IP as well as get better peering for the few family members that I share with. Plus I wanted to see if I could make it work.
I have a fully automated Letsencrypt setup for NGINX, which isn't completely necessary, but when connecting locally I wanted trusted certs without having to roll my own CA and load the signer certificate everywhere. Also if I decide to stop using Cloudflare, then I'll have "real" certs on my endpoints so there won't be any trust issues.
I'm also using split DNS so hostnames within my home network resolve directly to the server hosting Haproxy.
Note: This works with the web client, Android TV, FireTV, Android. Chromecast via Android Plex doesn't work, if you use the webclient to cast to Chromecast it works fine.
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
user haproxy
group haproxy
daemon
defaults
log global
option dontlognull
option http-server-close
timeout http-request 5s
timeout connect 5000
timeout client 30000
timeout server 30000
frontend tls_router
bind *:443
mode tcp
option tcpka
# Define known networks for later use
acl local src 127.0.0.0/8 192.168.1.0/24
acl cloudflare src 199.27.128.0/21 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/12 172.64.0.0/13 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32
#Rate-limiting measures, in case of DoS
# Table for connection tracking
stick-table type ip size 100k expire 30s store conn_cur
# Allow known CloudFlare IPs to bypass the rate-limiting
tcp-request connection accept if cloudflare
# Reject connection if client has more than 10 open
tcp-request connection reject if { src_conn_cur ge 10 }
tcp-request connection track-sc1 src
# Max inspection delay for SNI routing
tcp-request inspect-delay 2s
# Accept only SSL/TLS traffic
tcp-request content accept if { req_ssl_hello_type 1 }
# If SNI hostname is known server AND is from known IP, pass to backend
# Plex
acl plex_sni req_ssl_sni -i plex.example.com
# Root hostname
acl root_sni req_ssl_sni -i example.com
use_backend plex_tls if plex_sni cloudflare or plex_sni local
use_backend example_tls if root_sni cloudflare or root_sni local
default_backend example_tls
backend plex_tls
mode tcp
option tcpka
server server_plex_tls localhost:7443 send-proxy
backend example_tls
mode tcp
option tcpka
server server_example_tls localhost:443 send-proxy
This is a nice proxylog line
log_format proxyLog '[$time_local] $real_ip - $remote_user - $server_name to: $upstream_addr: $status / upstream $upstream_status $request upstream_re
sponse_time $upstream_response_time msec $msec request_time $request_time body: $request_body';
# plex reverse proxy
#
# Intended to sit downstream from HAProxy, uses proxy protocol.
# Allows traffic from the local network, or from WAN w/ CloudFlare client cert
#
server {
# Using proxy protocol to get client info passed from HAProxy
listen 127.0.0.1:7443 ssl http2 proxy_protocol;
listen [::1]:7443 ssl http2 proxy_protocol;
keepalive_timeout 180;
proxy_bind 127.1.1.1;
# Log Rewrites
rewrite_log on;
access_log /var/log/nginx/plex_example_com.log proxyLog;
#error_log /var/log/nginx/plex_error.log debug;
# Who the hell am I?! Where's my stuff?!
server_name plex.example.com;
root /var/www/plex.example.com/public_html;
# Import some solid TLS settings
include snippets/solidtls.conf;
# Certs
ssl_certificate /srv/letsencrypt/certs/plex.example.com/fullchain.pem;
ssl_certificate_key /srv/letsencrypt/certs/plex.example.com/privkey.pem;
ssl_trusted_certificate /srv/letsencrypt/certs/plex.example.com/fullchain.pem;
ssl_client_certificate /etc/nginx/ssl/cf-origin-pull-ca.pem;
# Client cert optional. We'll do our own access control rules in /
ssl_verify_client optional;
# Let's get the real client info from upstream proxy
include snippets/realip-cf-haproxy.conf;
# Set headers with real client info for downstream app
proxy_set_header Host $host;
proxy_set_header X-Real-IP $real_ip;
proxy_set_header X-Forwarded-For $real_ip;
proxy_set_header X-Forwarded-Proto $real_ip;
proxy_set_header X-Forwarded-Protocol $scheme;
# Turn off buffering for streaming purposes
proxy_buffering off;
# No request rewriting
proxy_redirect off;
# Send websocket data to the backend, courtesy @[member="Karbowiak"] on Emby forum
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Plex headers
# As of 0.9.17.0 this shouldn't be needed anymore since Plex now ignores case in headers
# proxy_set_header X-Plex-Client-Identifier $http_x_plex_client_identifier;
# proxy_set_header X-Plex-Device $http_x_plex_device;
# proxy_set_header X-Plex-Device-Name $http_x_plex_device_name;
# proxy_set_header X-Plex-Platform $http_x_plex_platform;
# proxy_set_header X-Plex-Platform-Version $http_x_plex_platform_version;
# proxy_set_header X-Plex-Product $http_x_plex_product;
# proxy_set_header X-Plex-Token $http_x_plex_token;
# proxy_set_header X-Plex-Version $http_x_plex_version;
# proxy_set_header X-Plex-Nocache $http_x_plex_nocache;
# proxy_set_header X-Plex-Provides $http_x_plex_provides;
# proxy_set_header X-Plex-Device-Vendor $http_x_plex_device_vendor;
# proxy_set_header X-Plex-Model $http_x_plex_model;
# proxy_set_header X-Plex-Container-Start $http_x_plex_container_start;
# proxy_set_header X-Plex-Container-Size $http_x_plex_container_size;
# Test if IP from HAProxy is local - tweak to match your network
set $is_local_client false;
if ( $proxy_protocol_addr ~ 192.168.1.* ) {
set $is_local_client true;
}
# Force the use of our custom robots.txt
location /robots.txt {
try_files $uri /var/www/plex.example.com/public_html/robots.txt;
}
# Catch-all. Performs authentication based on client IP or certificate.
location / {
# Redirect if not an options request.
if ($request_method != OPTIONS ) {
set $test A;
}
if ($http_x_plex_device_name = '') {
set $test "${test}B";
}
if ($arg_X-Plex-Device-Name = '') {
set $test "${test}C";
}
if ($test = ABC) {
rewrite ^/$ https://$http_host/web/index.html;
}
# We'll return 418 later if client is authorized
error_page 418 = @authorized;
#Access rules
# Allow local clients without client cert
if ( $is_local_client = true ) { return 418; }
# Does client have valid client certificate?
if ( $ssl_client_verify = "SUCCESS" ) { return 418; }
# Deny all others, return 403 unauthorized
return 403;
}
# Handles 418 responses for authorized users. Does the actual proxying.
# Using the real IP of the Plex server instead of localhost
location @authorized {
proxy_pass https://192.168.1.20:32400;
}
}
#Local traffic, including HAProxy on localhost
set_real_ip_from 127.0.0.0/8;
#CloudFlare
#These need to be updated from Cloudflare as they may have changed
set_real_ip_from 199.27.128.0/21;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
#Default state, assume local traffic/proxy protocol header
set $forwarded_ip_header proxy_protocol;
set $real_ip $proxy_protocol_addr;
# Is Cloudflare header set? Use that header/addr instead of proxy_protocol
if ($http_cf_connecting_ip) {
set $forwarded_ip_header CF-Connecting-IP;
set $real_ip $http_cf_connecting_ip;
}
# Tell Nginx which header to use
real_ip_header $forwarded_ip_header;
# Trustworthy SSL/TLS settings in a box
#
ssl on;
gzip off; # gzip allows for some attacks
# General params
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Always use custom-generated DH params >2048 bits
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
#Intermediate cipher config
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
#Modern cipher config
#ssl_protocols TLSv1.1 TLSv1.2;
#ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
#Perfect ssllabs score cipher list. Not super usable, works for CF-only setups
#ssl_protocols TLSv1.2;
#ssl_ciphers AES256+EECDH:AES256+EDH:!aNULL;
# No downgrade attacks
ssl_prefer_server_ciphers on;
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
# Stapling
ssl_stapling on;
ssl_stapling_verify on;
# DNS resolvers to use for cert verification
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
@camjac251
Copy link

camjac251 commented May 24, 2016

So it seems like it's not forwarding to it. Even though the reverse proxy is "working" with no errors, something isn't being passed through the CDN. I think there might a problem with the usage that I'm trying it under. With the config, it looks like you have a local network set up to allow for it to be connecting remotely while keeping Plex from reaching that port 32400 if it's not forwarded, which could force it to use a custom URL. In my case I don't have access to such a network where I am able to setup an isolated non 127.*,localhost IP (since that'd automatically be authenticated becoming insecure) and am unable to block the port 32400 as that would remove the possibility of routing through a more secure "local" to "local" network like you have. I believe that is why it is not working, because Plex can resolve the public IP just fine so it resorts to that skipping the CDN entirely

@doonga
Copy link
Author

doonga commented May 24, 2016

Yep I have it working just fine. A note though, better peering probably won't get you too much in better speeds (in the US at least.) It really is there to help reduce jitter and to hide your real IP.

You do need to go into Plex and make your public port match what you are using in haproxy which should be 443. You also need to make your Custom Server Access URLs: https://plex.yourdomain.com (or whatever you use, the https:// part is what's important.)

@camjac251
Copy link

Did you portforward 32400 on your local network, I believe that is why mine wasn't working. If you didn't then that would mean that my plex would auto resolve to the IP and port 32400 whereas if yours couldn't reach the internet then it would force itself to use the custom url.

I had my custom server url set as well. What do you mean by making sure the public port matches haproxy? in the url? in the server settings? my custom url although was set to https://plex.mydomain.com:443 with the port.

@doonga
Copy link
Author

doonga commented May 25, 2016

No I only have 443 port forwarded. On the Remote Access tab in the Plex settings, make sure you set the remote port manually as 443. The Custom Server Access URL doesn't need the port on it, that's the purpose of the port number on the Remote Access tab.

@skius
Copy link

skius commented Feb 6, 2017

Hi! This works for me, thank you so much!

Is there a way to run other HTTPS services on the same machine though? (Such as PlexRequests or rutorrent)
I assume I'd have to change something in the haproxy config, I'm not sure what however.

@niayh
Copy link

niayh commented Feb 26, 2017

I managed to get your code working; however, like @camjac251 my stream isn't being passed through cloudflare, just the library itself.

tcptrack reveals that during playback, the source is serverip:32400, so effectively my server's ip is still being leaked. Blocking port 32400 has the effect of stopping media playback; however, I can still browse the media library with thumbnails loading for all objects.

I have set the custom url and public port to 443, yet it doesn't seem to affect it overall. I'm going to keep tweaking some stuff on my end, but not sure there's anyway that it can be forced.

@kiwimato
Copy link

kiwimato commented Nov 4, 2023

Chromecast via Android Plex doesn't work, if you use the webclient to cast to Chromecast it works fine.

This is true, with and without this Nginx solution. I tried using Nginx just to try something else because even without it I al ways get "Something went wrong" in Plex whenever I cast from my Android app. However, if i login from the phone browser and cast from there it works just fine.

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment