[GUIDE] HOWTO: Reverse Proxy Header Hardening (CSP + Security Headers)

Disclaimer

I, by no means, am a networking specialist nor a developer. I’m the learning-by-doing type, so if you do have anything to add, every bit is appreciated. I’d like this to be as accurate as it can get, relatively speaking.

I strongly advise you to first read the entire post, before just copying and pasting what I’ve done into your setup.


guide last updated/verified: 28.10.2024
headers last updated/verified: 28.08.2025
Plex Web Version 4.147.1

INTRO

CSP stands for Content Security Policy. Basically it’s a do’s and don’t’s instruction list for the browser, telling it what it’s allowed to load from where. For example, Plex uses Google Fonts which aren’t stored on your server, but rather on Google’s. So you can say “hey browser, only download fonts from fonts.googleapis.com and block all other font sources.”
This serves twofold.

  1. This prevents the clients browser loading questionable sources in case your back-end (in this case Plex) gets compromised. Obviously this only works, if your reverse proxy isn’t compromised as well.
  2. Prevents malicious Browserplugins from injecting code to run on the clients system (e.g. miners)

General advice moving forward:
Create a file CSP_Plex.conf and insert include CSP_Plex.conf; in your server { block. That way you only have to edit the CSP_Plex.conf and not search for the relevant parts in your nginx.conf.


Content-Security-Policy:

I got an D+ rating on Mozilla’s website security-check. Only lacking a Cross-origin-Resource-Sharing (CORS) header, which just can’t be scanned by Mozillas tool, because it gets 401’d (because it doesn’t follow redirects to /web). Also including unsafe-eval isn’t something you wanna do typically, as the name already suggests. But unless you’re willing to rewrite the entire Plex WebUI, there’s no way around this.

Getting going:

  1. Combating Inline Scripting: Three Options*
    a) don’t bother with inline-script hashing and replace $script_hashes and $style_hashes
    below with unsafe-inline each
    b) Run my CSP_hasher.sh on your plex machine (requires libxml2-utils and access to /usr/lib/plexmediaserver - an empty instance of plex docker image will do) and the output will be the two lines containing sha256 hashes of all the unsavory inline-scripting Plex does (the script goes through all HTML files in the plex directory and searches them for inline-scripting). This script might break in the future.

    NOTE: These change with every web interface update, so you’ll have to re-run the script and edit this config to reflect the changes. I haven’t gotten around to fully automating this part yet.

    c) (easy-mode) use my hashes at the risk of them going out of date at some point if I don’t update them. In that case you can always fall back to a) or b)
# This is what the script outputs. 
# You can use these hashes (same as below)
set $script_hashes "'sha256-GcwLRDW7XyLvQjVnUDDeLp/npZkD9vkKUHdIa+PCuPM=' 'sha256-pKO/nNgeauDINvYfxdygP3mGssdVQRpRNxaF7uPRoGM=' 'sha256-qbTI7sHMvfM0qQov+1/M98MbZETjZsihqU7meLHFxkk=' 'sha384-vs8NcKmO1v9WEbLVzRYOIDcjM/zX4Uv1VFSTD2DXWpmsjlgJF6qeufhZYmPXcHBS' 'sha384-WailfaHzkPp4r7kZf8QgKfBMgKNCGiddDvQzTHfbnAjkXDhCBy9y4m2+D0yk3RmC'";
set $style_hashes "'sha256-ZdHxw9eWtnxUb3mk6tBS+gIiVUPE3pGM470keHPDFlE=' 'sha256-dd4J3UnQShsOmqcYi4vN5BT3mGZB/0fOwBA72rsguKc='";
  1. Below replace <PLEXDOMAIN>, and if applicable <LOCAL.PLEXDOMAIN>, with your plex domains
  2. If you’re using anything besides Chrome, move everything from script-src-elem to script-src
# last updated/verified: 28.08.2025

# replace <PLEXDOMAIN> with your public plex domain and <LOCAl.PLEXDOMAIN> with your locally routable plex domain if applicable. Remove if not.
set $plex_connect "https://<LOCAL.PLEXDOMAIN> wss://<LOCAL.PLEXDOMAIN> https://<PLEXDOMAIN> wss://<PLEXDOMAIN>";
set $plex_self "https://<LOCAL.PLEXDOMAIN> https://<PLEXDOMAIN>";

# insert script output here 1:1 
set $script_hashes "'sha256-GcwLRDW7XyLvQjVnUDDeLp/npZkD9vkKUHdIa+PCuPM=' 'sha256-pKO/nNgeauDINvYfxdygP3mGssdVQRpRNxaF7uPRoGM=' 'sha256-qbTI7sHMvfM0qQov+1/M98MbZETjZsihqU7meLHFxkk=' 'sha384-vs8NcKmO1v9WEbLVzRYOIDcjM/zX4Uv1VFSTD2DXWpmsjlgJF6qeufhZYmPXcHBS' 'sha384-WailfaHzkPp4r7kZf8QgKfBMgKNCGiddDvQzTHfbnAjkXDhCBy9y4m2+D0yk3RmC'";
set $style_hashes "'sha256-ZdHxw9eWtnxUb3mk6tBS+gIiVUPE3pGM470keHPDFlE=' 'sha256-dd4J3UnQShsOmqcYi4vN5BT3mGZB/0fOwBA72rsguKc='";
# Uncomment below if you're having issues with the hashes
#set $script_hashes "'unsafe-inline'";
#set $style_hashes "'unsafe-inline'";

# these are default connect sources for plex. No need to edit them unless Plex adds something new
set $default_plex_connect "https://*.ingest.sentry.io  https://plex.tv https://*.plex.tv https://*.plex.direct https://*.plex.direct:32400 wss://*.plex.direct wss://*.plex.direct:32400 https://*.plex.services wss://*.plex.tv wss://*.syncplay.plex.services:7777 wss://*.syncplay.plex.services:7776";

# this adds the actual header, no need to edit, since all variables can be edited above
add_header content-security-policy "default-src 'none'; prefetch-src 'self'; script-src 'unsafe-eval' 'report-sample'; script-src-elem https://www.gstatic.com 'self' 'strict-dynamic' $script_hashes $cf_insight; style-src 'report-sample' 'self' 'unsafe-hashes' $style_hashes https://fonts.googleapis.com; style-src-attr 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; connect-src 'self' $plex_connect $default_plex_connect; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.plex.direct:*; frame-ancestors 'none'; img-src 'self' $plex_self https://*.plex.tv https://plex.tv blob: data: https://*.plex.direct:*; manifest-src 'self'; media-src 'self' $plex_self https://video.internetvideoarchive.net data: blob: https://*.plex.direct:*; worker-src 'none'; form-action 'self'; upgrade-insecure-requests" always;
  1. (optional): remove https://*.ingest.sentry.io from connect-src if you don’t want to send statistics to Plex Inc.
  2. add the following code to the existing stuff from above e.g. everything into CSP_Plex.conf:
# basic dont-allow-anything-funky and force SSL headers
# don't forget to fill out <PLEXDOMAIN> in the last row. Only ONE is allowed, NO wildcards. See step 10. for workaround
add_header referrer-policy "same-origin" always;
add_header strict-transport-security "max-age=31536000; includesubdomains; preload" always;
add_header x-content-type-options "nosniff" always;
add_header x-frame-options sameorigin;
add_header x-xss-protection "1; mode=block" always;
add_header access-control-allow-origin https://<PLEXDOMAIN>; # note: if you want to keep access through app.plex.tv see step 10.

you can check the final contents of CSP_Plex.conf for syntax errors here (by Google) (thx @egranty)

  1. Save CSP_Plex.conf in your reverse proxy directory (opnsense:/usr/local/etc/nginx)
  2. Edit nginx.conf, search for your Plex server { block and somewhere before the location / { block insert include CSP_Plex.conf;
  3. Save and restart nginx with nginx reload (might be different on other systems)
  4. test plex
10. If hosting Plex on multiple subdomains (e.g. `plex.mydomain` and `plex.local.mydomain` do this, too
  1. Variable CORS header (what’s that?).
    10.1) replace https://<PLEXDOMAIN> in last line from your CSP_Plex.conf with $CORS_domain
    10.2) Paste the following block into the http { block
# replace <PLEXDOMAIN>
map $http_origin $CORS_domain {
	"~*<PLEXDOMAIN>$" "$http_origin";
	"~*app.plex.tv$" "$http_origin";
	default "https://<PLEXDOMAIN>";
}

Done. Feedback highly encouraged! Fell free to PM me if you have any issues.
Also, if you want to add even more security to your setup (wwhhaahaat?), maybe check out how to setup a WAF (Web Application Firewall) for Plex.

Sidenote: If you’re proxying through Cloudflares CDN you need to disable “Rocket Loader” and “Browser Integrity check” via Page Rule. Also, you’ll at least need to bypass https://plexdomain/library/* in order for seeking to work.


Here are the links for the other CSP threads:
[Content Security Policy (CSP) for Plex Web / Reverse Proxy] closed;
[Creating a Content Security Policy for Plex Web] closed;

1 Like

This CSP has errors:

  • the https://*.plex.tv/* source has a wrong syntax, you can’t use * in the path-part.
  • the https://*.plex.direct* has a wrong syntax, possible : is missed before port number (https://*.plex.direct:*).

This CSP has errors too.
The same issue with https://*.plex.tv/* and https://www.gstatic.com/* - incorrect sources. Use https://*.plex.tv and https://www.gstatic.com instead of.

1 Like

thx for the input. never was sure about the syntax. could’ve thought about the “:” tho. the “no * in path” is a really nice to know. maybe that was why my smart-tv app was crashing constantly (hopefully)

About ‘:’ - you shown “config including missing options” which has connect-src ... https://*.plex.direct:* ... with the ‘:’. But the “opnsense GUI config” has the same connect-src ... https://*.plex.direct* ... without ‘:’.

Since domain name plex.direct exists, I just figured that https://*.plex.direct:* is the correct source because :* means any port number. Whereas https://*.plex.direct allows connects via port 443 only (it’s standard port for https:).

I cannot be 100% sure if this source is needed in the connect-src directive, I just fixed it to the correct syntax.

The connect-src directive covers:
• the ping= attribute in the <a href='https//site.com/page.html' ping='https://site.com/click.php'>
• fetch()
• XMLHttpRequest()
• sendBeacon()
• WebSocket()
• EventSource()

It need to check what from above is used by plex and with which ports nbrs.

Changelog:

June 2022:
Updated Guide to adjust for inline scripting and variable plex domain

July 2022:
Updated Guide to implement best practices when using dynamic CORS headers

February 2023
Checked if stuff is still up-to-date and fixed some typos
Updated hashes for Web Version 4.100.1

May 2023
updated note on hosting behind Cloudflare

February 2024
updated CSP hasher

Updated Web Versions -> 4.136.1

March 2024
updated CSP hasher
Updated hashes for Web Version 4.125.1

June 2024
Updated hashes for Web Version 4.129.1

August 2024
Updated hashes for Web Version 4.133.0

September 2024
Updated hashes for Web Version 4.136.1

October 2024
Changed to use ‘strict-dynamic’ in script-src-elem. Updated hashes will only be tested with this feature (all common browsers support this feature)

Updated hashes for Web Version 4.138.0

Updated Web Versions -> 4.147.1

November 2024
Updated hashes for Web Version 4.140.0

February 2025
Updated hashes for Web Version 4.143.0

March 2025
Updated hashes for Web Version 4.145.1

May 2025
Updated hashes for Web Version 4.146.1

July 2025
Updated hashes for Web Version 4.147.1

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.