Skip to content

Instantly share code, notes, and snippets.

@tcarrio
Last active December 9, 2024 00:19
Show Gist options
  • Save tcarrio/d450d3974bbe2f6e56658405e6e3a236 to your computer and use it in GitHub Desktop.
Save tcarrio/d450d3974bbe2f6e56658405e6e3a236 to your computer and use it in GitHub Desktop.
Public Plex Media Server proxied via Tailscale behind Nginx

Overview

This project sets up a Public Plex Media Server securely proxied through Tailscale and managed by Nginx on a NixOS-based system. It is designed to provide remote access to your Plex server while leveraging Tailscale for private networking and Nginx for robust HTTP(S) proxying.

Features

  • Secure Proxying: Utilizes Nginx as a reverse proxy for handling HTTPS connections, configured with an ACME (Let's Encrypt) certificate for your domain.
  • Tailscale Integration: Simplifies remote network connectivity using Tailscale's private mesh VPN.
  • Optimized for Streaming: Nginx configuration includes settings to enhance streaming performance, support websockets, and reduce buffering for an optimal Plex experience.
  • Configurable Firewall: Ensures secure access by restricting allowed traffic to specific ports.
  • NixOS Configuration: Fully reproducible and declarative system setup.

Prerequisites

  • NixOS: This project assumes you're using NixOS for configuration.
  • Domain Name: A public domain (e.g., example.domain.net) is required for SSL setup.
  • Tailscale Account: Set up a Tailscale network to enable private connectivity.
  • Cloudflare DNS: Used for ACME DNS verification in this example.

Configuration Details

  • Nginx:
    • Configured as a reverse proxy for Plex, forwarding traffic from your public domain to the Plex server via Tailscale.
    • HTTPS enforced using ACME certificates.
    • HTTP2 enabled for better streaming performance.
  • Tailscale:
    • Proxy traffic to your Plex server through the Tailnet.
  • Firewall:
    • Ports restricted to ensure secure access (e.g., only TCP and UDP port 443 are allowed).
  • System Setup:
    • Uses declarative NixOS configuration, including disk setup and bootloader configuration.

Example NixOS Configuration

The provided configuration.nix file demonstrates:

  • Setting up Tailscale for private networking.
  • Configuring Nginx as a reverse proxy for Plex.
  • Securing Nginx with ACME/Let's Encrypt certificates.
  • Setting up a secure DNS resolver for Tailscale hosts.

Usage

  1. Clone the repository or copy the example configuration.
  2. Replace placeholders like example.domain.net and example.owl-bear.ts.net with your actual domain and Tailscale details.
  3. Add the configuration to your NixOS system configuration file, or import this file into your existing configuration.
  4. Run nixos-rebuild switch to apply the changes.

Notes

  • This example omits parts of the full NixOS configuration for brevity. Additional options, such as disk layout, host platform, and other services, can be added as needed.
  • Adjust Nginx settings (e.g., client_max_body_size) to suit your streaming and file upload requirements.
  • For full documentation on related NixOS modules, refer to:

⚠️ Disclaimer ⚠️

This configuration is intended as a starting point. Review and tailor it to meet your specific needs and security requirements before deploying it in production.

{ lib, config, ... }:
let
external_domain = "example.domain.net";
tailnet_domain = "example.owl-bear.ts.net";
# Necessary for resolving Tailnet hosts inside Nginx
tailnet_dns_resolver = "100.100.100.100";
in
{
######################################## NOTE ################################
# Further NixOS configuration omitted, but you would be able to define more #
# options and services for your needs outside of the following configuration.#
# An example of this is `hostPlatform`, but plenty more :) #
##############################################################################
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
# Disk and boot configuration for post-switch in Digital Ocean droplet env
boot.loader.grub.devices = [ "/dev/vda" ];
disko.devices = {
disk = {
vda = {
device = "/dev/disk/by-label/nixos";
name = "/dev/vda";
type = "disk";
content = {
type = "gpt";
partitions = {
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
# Securely mount Age secret for Cloudflare DNS verification config
age.secrets.cloudflare-dns-verification = {
file = ./path/to/secrets/acme/cloudflare.age;
owner = "root";
group = "root";
mode = "400";
};
# ACME NixOS Docs: https://wiki.nixos.org/wiki/ACME
security.acme = {
acceptTerms = true;
defaults.email = "[email protected]";
certs = {
"${external_domain}" = {
domain = external_domain;
group = "nginx";
dnsProvider = "cloudflare";
# https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#EnvironmentFile=
environmentFile = config.age.secrets.cloudflare-dns-verification.path;
};
};
};
oxc.services.tailscale.enable = true;
oxc.services.tailscale.autoconnect = false;
services.smartd.enable = lib.mkForce false;
# Plex NixOS Docs: https://nixos.wiki/wiki/Plex
services.nginx = {
enable = true;
# give a name to the virtual host. It also becomes the server name.
virtualHosts."${external_domain}" = {
# Since we want a secure connection, we force SSL
forceSSL = true;
# http2 can more performant for streaming: https://blog.cloudflare.com/introducing-http2/
http2 = true;
# Provide the ssl cert and key for the vhost
sslCertificate = "/var/lib/acme/${external_domain}/fullchain.pem";
sslCertificateKey = "/var/lib/acme/${external_domain}/key.pem";
extraConfig = ''
#Some players don't reopen a socket and playback stops totally instead of resuming after an extended pause
send_timeout 100m;
resolver ${tailnet_dns_resolver} valid=30s;
# Why this is important: https://blog.cloudflare.com/ocsp-stapling-how-cloudflare-just-made-ssl-30/
ssl_stapling on;
ssl_stapling_verify on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
#Intentionally not hardened for security for player support and encryption video streams has a lot of overhead with something like AES-256-GCM-SHA384.
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';
# Forward real ip and host to Plex
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $server_addr;
proxy_set_header Referer $server_addr;
proxy_set_header Origin $server_addr;
# Plex has A LOT of javascript, xml and html. This helps a lot, but if it causes playback issues with devices turn it off.
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_proxied any;
gzip_types text/plain text/css text/xml application/xml text/javascript application/x-javascript image/svg+xml;
gzip_disable "MSIE [1-6]\.";
# Nginx default client_max_body_size is 1MB, which breaks Camera Upload feature from the phones.
# Increasing the limit fixes the issue. Anyhow, if 4K videos are expected to be uploaded, the size might need to be increased even more
client_max_body_size 100M;
# Plex 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;
# Websockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Buffering off send to the client as soon as the data is received from Plex.
proxy_redirect off;
proxy_buffering off;
'';
locations."/" = {
proxyPass = "https://${tailnet_domain}/";
};
};
};
networking.firewall = {
allowedTCPPorts = [ 443 ];
allowedUDPPorts = [ 443 ];
};
}
@tcarrio
Copy link
Author

tcarrio commented Dec 9, 2024

This showcases only the proxy side. An important note I'll include is that the proxyPass configured is to another Nginx-fronted HTTPS proxy for the Plex Media Server. Hence why the target is https and standard 443 port. If you don't have an Nginx server in front of your Plex, you would target the default 32400 unless configured differently.

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