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.
- 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.
- 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:
- Combating Inline Scripting: Three Options*
a) don’t bother with inline-script hashing and replace$script_hashesand$style_hashes
below withunsafe-inlineeach
b) Run my CSP_hasher.sh on your plex machine (requireslibxml2-utilsand 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='";
- Below replace
<PLEXDOMAIN>, and if applicable<LOCAL.PLEXDOMAIN>, with your plex domains - If you’re using anything besides Chrome, move everything from
script-src-elemtoscript-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;
- (optional): remove
https://*.ingest.sentry.iofromconnect-srcif you don’t want to send statistics to Plex Inc. - 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)
- Save
CSP_Plex.confin your reverse proxy directory (opnsense:/usr/local/etc/nginx) - Edit
nginx.conf, search for your Plexserver {block and somewhere before thelocation / {block insertinclude CSP_Plex.conf; - Save and restart nginx with
nginx reload(might be different on other systems) - test plex
10. If hosting Plex on multiple subdomains (e.g. `plex.mydomain` and `plex.local.mydomain` do this, too
- Variable CORS header (what’s that?).
10.1) replacehttps://<PLEXDOMAIN>in last line from yourCSP_Plex.confwith$CORS_domain
10.2) Paste the following block into thehttp {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;