├── .gitignore ├── README.md ├── all.sh ├── assets ├── jellyfin.svg ├── jellytin-dark.svg └── jellytin-light.svg ├── authentik ├── README.md ├── docker-compose.yml └── template.env ├── authentik_ldap ├── .env ├── README.md ├── docker-compose.yml └── template.env ├── docker_install.sh ├── docs ├── authentik_to_jellyfin.md ├── cloudflare_domain.md ├── jellyfin_client_whitelist.conf ├── jellyfin_client_whitelist.md ├── jellyfin_ldap.md ├── jellyfin_ldap_users.md ├── npm_to_authentik.md ├── tailscale_acl.jsonc ├── tailscale_configure.md ├── vps_harden.md └── vps_routing.md ├── nginx_proxy_manager ├── README.md ├── docker-compose.yml ├── mounts │ └── data │ │ └── nginx │ │ └── custom │ │ ├── README.md │ │ ├── http_top.conf │ │ └── server_proxy.conf └── template.env ├── npm_tunnel ├── README.md ├── docker-compose.yml └── template.env └── vps_tunnel ├── README.md ├── docker-compose.yml ├── mounts └── data │ └── nginx │ └── custom │ ├── README.md │ └── server_proxy.conf └── template.env /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | venv/* 3 | **/.DS_Store 4 | 5 | secret.env 6 | .env 7 | !authentik_ldap/.env 8 | 9 | */mounts 10 | 11 | !/nginx_proxy_manager/mounts 12 | /nginx_proxy_manager/mounts/* 13 | !/nginx_proxy_manager/mounts/data 14 | /nginx_proxy_manager/mounts/data/* 15 | !/nginx_proxy_manager/mounts/data/nginx 16 | /nginx_proxy_manager/mounts/data/nginx/* 17 | !/nginx_proxy_manager/mounts/data/nginx/custom 18 | 19 | !/vps_tunnel/mounts 20 | /vps_tunnel/mounts/* 21 | !/vps_tunnel/mounts/data 22 | /vps_tunnel/mounts/data/* 23 | !/vps_tunnel/mounts/data/nginx 24 | /vps_tunnel/mounts/data/nginx/* 25 | !/vps_tunnel/mounts/data/nginx/custom 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | # Jellytin 7 | 8 | Put your local Jellyfin server in a tin, and securely serve it up on the internet 🚀 9 | 10 | 11 | ### Intro 12 | This project is especially helpful if you: 13 | 1) Have a local Jellyfin server that you want to access over the internet 14 | 1) Do *not* currently have any infrastructure to expose services to the internet 15 | 1) Wish to hide + secure Jellyfin behind an identity provider 16 | 1) Wish to use Jellyfin [clients](https://jellyfin.org/clients/) (Android, etc.) 17 | 18 | If you're wondering, "why can't I just expose my Jellyfin server to the internet?" 19 | I recommend reading [Collection of potential security issues in Jellyfin](https://github.com/jellyfin/jellyfin/issues/5415) 20 | 21 | The final deployment looks like this: 22 | 23 | 👤 -> `VPS` -> `Nginx` -> `Tailscale` -> `Nginx` -> `Authentik` -> `Jellyfin` 24 | 25 | 26 | ### Dependencies 27 | * Virtual Private Server (VPS) 28 | * [BuyKVM](https://buyvm.net/kvm-dedicated-server-slices/) is $2/month, 29 | but any VPS with an IPv4 address will do 30 | * Additional layer of security, e.g. hide your personal IP 31 | * [Nginx Proxy Manager](https://nginxproxymanager.com/) 32 | * Management UI for Nginx 33 | * [Tailscale](https://tailscale.com/) 34 | * Tunnel from VPS to your local network 35 | * [Authentik](https://goauthentik.io/) 36 | * Provide authentication via LDAP, SSO, etc. 37 | * [Jellyfin](https://jellyfin.org/downloads/) 38 | * You should already have a Jellyfin server 39 | 40 | 41 | ### Future Improvements 42 | 43 | 1) Keep an eye on Jellyfin's [SSO plugin](https://github.com/9p4/jellyfin-plugin-sso) 44 | and incorporate it here, once it is stable and no longer "100% alpha software." 45 | 46 | 1) Await NPM's [Fail2Ban feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/39). 47 | 48 | 1) Await NPM's [CrowdSec feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/1131). 49 | 50 | 51 | ### Footnote 52 | If you're using a Raspberry Pi, then you will need the [64-bit OS](https://www.raspberrypi.com/news/raspberry-pi-os-64-bit/). 53 | 54 | 55 | # Instructions 56 | 57 | 58 | ### Install `docker` & `docker-compose` 59 | * Skip if already installed on your system 60 | * Otherwise, install via [./docker_install.sh](./docker_install.sh) 61 | 62 | ### Deploy Authentik 63 | * Deploy via [./authentik/](./authentik/) 64 | * Generalize yourself with [Outposts, Providers, & Applications](https://goauthentik.io/docs/terminology) 65 | * tl;dr to deploy an application in Authentik, you need an Outpost to service a Provider, which services an Application 66 | 67 | ### Deploy Nginx Proxy Manager 68 | * Deploy via [./nginx_proxy_manager/](./nginx_proxy_manager/) 69 | 70 | ### Purchase & Configure Cloudflare Domain 71 | * Configure via [./docs/cloudflare_domain.md](./docs/cloudflare_domain.md) 72 | 73 | ### Harden Your VPS 74 | * Harden via [./docs/vps_harden.md](./docs/vps_harden.md) 75 | 76 | ### Configure Tailscale 77 | * Configure via [./docs/tailscale_configure.md](./docs/tailscale_configure.md) 78 | 79 | ### Deploy VPS Tunnel 80 | * Deploy via [./vps_tunnel/](./vps_tunnel/) 81 | 82 | ### Deploy NPM Tunnel 83 | * Deploy via [./npm_tunnel/](./npm_tunnel/) 84 | 85 | ### Configure Tunnel Routing 86 | * Configure via [./docs/vps_routing.md](./docs/vps_routing.md) 87 | 88 | ### Configure Nginx -> Authentik 89 | * Configure via [./docs/npm_to_authentik.md](./docs/npm_to_authentik.md) 90 | 91 | ### Configure Authentik -> Jellyfin 92 | * Configure via [./docs/authentik_to_jellyfin.md](./docs/authentik_to_jellyfin.md) 93 | 94 | ### Deploy Authentik LDAP Service 95 | * Deploy via [./authentik_ldap/](./authentik_ldap/) 96 | 97 | ### Configure Jellyfin for LDAP Authentication 98 | * Configure via [./docs/jellyfin_ldap.md](./docs/jellyfin_ldap.md) 99 | 100 | ### Create Jellyfin Users via Authentik 101 | * Create via [./docs/jellyfin_ldap_users.md](./docs/jellyfin_ldap_users.md) 102 | 103 | ### Configure NPM to Enable Jellyfin Client Apps 104 | * Configure via [./docs/jellyfin_client_whitelist.md](./docs/jellyfin_client_whitelist.md) 105 | 106 | --- 107 | 108 | ### The End 🎉 109 | 110 | You can use the helper script at [`./all.sh`](./all.sh) to control this stack. 111 | -------------------------------------------------------------------------------- /all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ## 5 | # This is a helper script to run docker-compose 6 | # commands against all the stacks in this repo, 7 | # excluding ./vps_tunnel 8 | # 9 | # for example: 10 | # `./all.sh up -d` 11 | # `./all.sh ps` 12 | # `./all.sh down` 13 | ## 14 | 15 | dir_path() { 16 | # https://stackoverflow.com/a/43919044 17 | a="/$0"; a="${a%/*}"; a="${a:-.}"; a="${a##/}/"; BINDIR=$(cd "$a"; pwd) 18 | echo "$BINDIR" 19 | } 20 | DIR_PATH=`dir_path` 21 | 22 | for COMPOSE_DIR in 'authentik' 'authentik_ldap' 'nginx_proxy_manager' 'npm_tunnel'; do 23 | (cd "$DIR_PATH/$COMPOSE_DIR" && docker-compose "$@") || echo '^^ ERROR ^^' 24 | echo '' 25 | done 26 | -------------------------------------------------------------------------------- /assets/jellyfin.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /assets/jellytin-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Fabric.js 3.5.0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /assets/jellytin-light.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Fabric.js 3.5.0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /authentik/README.md: -------------------------------------------------------------------------------- 1 | # Deploy Authentik 2 | 3 | 4 | The contents in this directory have been modified from the Authentik [docs](https://goauthentik.io/docs/installation/docker-compose/). 5 | 6 | * Copy the variables template via `cp ./template.env ./.env` 7 | * Set the `SECRET` values within `./.env`, as described by the above docs 8 | * Deploy via `docker-compose up -d` 9 | 10 | --- 11 | 12 | Login to the gui via below and set the admin password: 13 | > [`http://__HOST_IP__:9000/if/flow/initial-setup/`](http://__HOST_IP__:9000/if/flow/initial-setup/) 14 | 15 | ☝️ Very important -- don't skip that step ⚠️ 16 | 17 | --- 18 | 19 | * Open the admin dashboard gui, and go to `Flows & Stages` > `Stages`. 20 | * Click the 📝 icon to update `default-authentication-identification` 21 | * Set `Password stage` = `default-authentication-password` 22 | 23 | --- 24 | 25 | After deploying, you can access the gui via [`http://__HOST_IP__:9000`](http://__HOST_IP__:9000) 26 | -------------------------------------------------------------------------------- /authentik/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.4' 3 | 4 | services: 5 | authentik-postgresql: 6 | image: postgres:12-alpine 7 | restart: unless-stopped 8 | healthcheck: 9 | test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] 10 | start_period: 20s 11 | interval: 30s 12 | retries: 5 13 | timeout: 5s 14 | volumes: 15 | - authentik_database:/var/lib/postgresql/data 16 | environment: 17 | - POSTGRES_DB=$PG_DB 18 | - POSTGRES_PASSWORD=$PG_PASS 19 | - POSTGRES_USER=$PG_USER 20 | env_file: 21 | - .env 22 | authentik-redis: 23 | image: redis:alpine 24 | restart: unless-stopped 25 | healthcheck: 26 | test: ["CMD-SHELL", "redis-cli ping | grep PONG"] 27 | start_period: 20s 28 | interval: 30s 29 | retries: 5 30 | timeout: 3s 31 | authentik-media-chown: 32 | image: privatebin/chown:$CHOWN_TAG 33 | read_only: true 34 | command: 1000:1000 /mnt 35 | volumes: 36 | - authentik_media:/mnt 37 | authentik-server: 38 | image: ghcr.io/goauthentik/server:$AUTHENTIK_TAG 39 | restart: unless-stopped 40 | depends_on: 41 | - authentik-media-chown 42 | command: server 43 | environment: 44 | AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql 45 | AUTHENTIK_POSTGRESQL__NAME: $PG_DB 46 | AUTHENTIK_POSTGRESQL__PASSWORD: $PG_PASS 47 | AUTHENTIK_POSTGRESQL__USER: $PG_USER 48 | AUTHENTIK_REDIS__HOST: authentik-redis 49 | # AUTHENTIK_ERROR_REPORTING__ENABLED: "true" 50 | # WORKERS: 2 51 | env_file: 52 | - .env 53 | volumes: 54 | - ./mounts/custom_templates/:/templates 55 | - authentik_geoip:/geoip 56 | - authentik_media:/media 57 | ports: 58 | - "0.0.0.0:$AUTHENTIK_PORT_HTTP:9000" 59 | - "0.0.0.0:$AUTHENTIK_PORT_HTTPS:9443" 60 | networks: 61 | - authentik 62 | - default 63 | authentik-worker: 64 | image: ghcr.io/goauthentik/server:$AUTHENTIK_TAG 65 | restart: unless-stopped 66 | depends_on: 67 | - authentik-media-chown 68 | command: worker 69 | environment: 70 | AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql 71 | AUTHENTIK_POSTGRESQL__NAME: $PG_DB 72 | AUTHENTIK_POSTGRESQL__PASSWORD: $PG_PASS 73 | AUTHENTIK_POSTGRESQL__USER: $PG_USER 74 | AUTHENTIK_REDIS__HOST: authentik-redis 75 | # AUTHENTIK_ERROR_REPORTING__ENABLED: "true" 76 | env_file: 77 | - .env 78 | # This is optional, and can be removed. If you remove this, the following will happen 79 | # - The permissions for the /media folders aren't fixed, so make sure they are 1000:1000 80 | # - The docker socket can't be accessed anymore 81 | #user: root 82 | volumes: 83 | - ./mounts/custom_templates/:/templates 84 | #- /var/run/docker.sock:/var/run/docker.sock 85 | - authentik_geoip:/geoip 86 | - authentik_media:/media 87 | authentik-geoipupdate: 88 | image: maxmindinc/geoipupdate:v4.9 89 | restart: unless-stopped 90 | volumes: 91 | - authentik_geoip:/usr/share/GeoIP 92 | environment: 93 | GEOIPUPDATE_EDITION_IDS: "GeoLite2-City" 94 | GEOIPUPDATE_FREQUENCY: "8" 95 | env_file: 96 | - .env 97 | volumes: 98 | authentik_database: 99 | driver: local 100 | authentik_geoip: 101 | driver: local 102 | authentik_media: 103 | driver: local 104 | networks: 105 | authentik: 106 | name: authentik 107 | -------------------------------------------------------------------------------- /authentik/template.env: -------------------------------------------------------------------------------- 1 | AUTHENTIK_PORT_HTTP=9000 2 | AUTHENTIK_PORT_HTTPS=9443 3 | AUTHENTIK_SECRET_KEY=SECRET 4 | AUTHENTIK_TAG=latest # AUTHENTIK_TAG=2022.8.2 at time of writing 5 | CHOWN_TAG=latest # CHOWN_TAG=1.34.1-musl-1.2.2-r3 at time of writing 6 | 7 | PG_DB=authentik 8 | PG_PASS=SECRET 9 | PG_USER=authentik 10 | 11 | AUTHENTIK_AUTHENTIK__GEOIP=/geoip/GeoLite2-City.mmdb 12 | GEOIPUPDATE_ACCOUNT_ID=SECRET 13 | GEOIPUPDATE_LICENSE_KEY=SECRET 14 | 15 | AUTHENTIK_EMAIL__FROM=SECRET 16 | AUTHENTIK_EMAIL__HOST=smtp.gmail.com # if you want to use gmail -- https://bit.ly/2yyc9EE 17 | AUTHENTIK_EMAIL__PASSWORD=SECRET 18 | AUTHENTIK_EMAIL__PORT=587 19 | AUTHENTIK_EMAIL__TIMEOUT=10 20 | AUTHENTIK_EMAIL__USE_SSL=false 21 | AUTHENTIK_EMAIL__USE_TLS=true 22 | AUTHENTIK_EMAIL__USERNAME=SECRET 23 | -------------------------------------------------------------------------------- /authentik_ldap/.env: -------------------------------------------------------------------------------- 1 | ../authentik/.env -------------------------------------------------------------------------------- /authentik_ldap/README.md: -------------------------------------------------------------------------------- 1 | # Deploy Authentik LDAP Outpost 2 | 3 | 4 | * Open the Authentik Admin Dashboard 5 | * Go to `Directory` > `Users` 6 | * Select `Create Service Account` 7 | * Name the user `service-ldap-outpost` 8 | * Go to `Applications` > `Providers` 9 | * Click `Create` 10 | * Select `LDAP Provider` 11 | * On the 2nd tab: 12 | * `Name` = `ldap-provider` 13 | * `Search Group` = `service-ldap-outpost` 14 | * `Base DN` = `DC=ldap,DC=__MY_SITE__,DC=__COM__` 15 | * Use default values for all other fields 16 | * Go to `Applications` > `Applications` 17 | * Click `Create` 18 | * `Name` = `ldap-application` 19 | * `Provider` = `ldap-provider` 20 | * Go to `Applications` > `Outposts` 21 | * Click `Create` 22 | * `Name` = `ldap-outpost` 23 | * `Type` = `LDAP` 24 | * `Integration` = `---------` 25 | * (We'll integrate by deploying via docker-compose) 26 | * `Applications` = `ldap-application` 27 | * Click `View Deployment Info` on the outpost you just created 28 | * (note that you must access the webui via [https](https://__HOST_IP__:9443)) 29 | * Copy the `AUTHENTIK_TOKEN` 30 | * Copy the variables template via `cp ./template.env ./secret.env` 31 | * Within `./secret.env`: 32 | * Update `AUTHENTIK_TOKEN` to the value you copied above 33 | * Update `AUTHENTIK_HOST_BROWSER` as necessary 34 | * Deploy via `docker-compose up -d` 35 | * Check that the deploy is working via `docker-compose logs` 36 | * You should see a message like `{"event":"Fetched outpost configuration", ...}` 37 | -------------------------------------------------------------------------------- /authentik_ldap/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.4' 3 | 4 | services: 5 | authentik-ldap-outpost: 6 | image: ghcr.io/goauthentik/ldap:$AUTHENTIK_TAG 7 | restart: unless-stopped 8 | env_file: 9 | - secret.env 10 | environment: 11 | AUTHENTIK_HOST: http://authentik-server:9000 12 | ports: 13 | - 389:3389 14 | - 636:6636 15 | networks: 16 | - authentik 17 | networks: 18 | authentik: 19 | name: authentik 20 | -------------------------------------------------------------------------------- /authentik_ldap/template.env: -------------------------------------------------------------------------------- 1 | AUTHENTIK_HOST_BROWSER=https://auth.__MY_SITE__.__COM__ 2 | AUTHENTIK_INSECURE=false 3 | AUTHENTIK_TOKEN=SECRET 4 | -------------------------------------------------------------------------------- /docker_install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # install docker 5 | curl -sSL https://get.docker.com | sh 6 | sudo usermod -aG docker "$USER" # assuming current $USER will run docker 7 | exec sudo su -l "$USER" # https://superuser.com/a/609141 8 | docker run hello-world # test the install 9 | sudo systemctl enable docker # start docker on boot 10 | 11 | # install docker-compose 12 | sudo apt-get install libffi-dev libssl-dev 13 | sudo apt install python3-dev 14 | sudo apt-get install -y python3 python3-pip 15 | sudo pip3 install docker-compose 16 | -------------------------------------------------------------------------------- /docs/authentik_to_jellyfin.md: -------------------------------------------------------------------------------- 1 | # Configure Authentik -> Jellyfin 2 | 3 | * Open the Authentik Admin Dashboard 4 | * Go to `Directory` > `Groups` 5 | * `Create` two groups: 6 | 1) `jellyfin-users` 7 | 1) `jellyfin-admins` 8 | * Go to `Applications` > `Providers` 9 | * Click `Create` 10 | * Select `Proxy Provider` 11 | * On the 2nd tab: 12 | * `Name` = `jellyfin-provider` 13 | * `Authorization flow` = `Authorize Application (default-provider-authorization-implicit-consent)` 14 | * `External host` = `https://jf.__MY_SITE__.__COM__` 15 | * `Internal host` = `http://__JELLYFIN_IP__:__JELLYFIN_PORT__` 16 | * Go to `Applications` > `Applications` 17 | * Click `Create` 18 | * `Name` = `Jellyfin` 19 | * `Provider` = `jellyfin-provider` 20 | * Under `UI settings` 21 | * Upload an `Icon`, e.g. [this](../assets/jellyfin.svg) 22 | * Click `Jellyfin` (the app you just created) > `Policy / Group / User Bindings` 23 | * `Create Binding` for each `Group`: 24 | 1) `jellyfin-users` 25 | 1) `jellyfin-admins` 26 | 1) `authentik Admins` 27 | * Go to `Applications` > `Outposts` 28 | * Click the 📝 icon to update `authentik Embedded Outpost` 29 | * Add `Jellyfin` to the list of `Applications` 30 | * In `Configuration`, ensure that you have `authentik_host: https://auth.__MY_SITE__.__COM__` 31 | * You should now be able to access Jellyfin via [`https://jf.__MY_SITE__.__COM__`](https://jf.__MY_SITE__.__COM__) 32 | -------------------------------------------------------------------------------- /docs/cloudflare_domain.md: -------------------------------------------------------------------------------- 1 | # Purchase & Configure Cloudflare Domain 2 | 3 | * Purchase your domain via [this](https://developers.cloudflare.com/registrar/get-started/register-domain) 4 | * We'll assume that you purchase a domain of `__MY_SITE__.__COM__` 5 | * Login to [cloudflare](https://cloudflare.com) 6 | * Add wildcard CNAME for subdomains 7 | * Go to `Websites` > `__MY_SITE__.__COM__` > `DNS` > `Add record` 8 | * `Type` = `CNAME` 9 | * `Name` = `*` 10 | * `Target` = `__MY_SITE__.__COM__` 11 | * `Proxy status` = Disabled 12 | * Configure `` 13 | * Sorry, this isn't fully documented... 14 | * ... feel free to open a PR & improve this =) 15 | * Cloudflare will recommend multiple security settings for you, so explore around the dashboard, for example: 16 | * Under DNS management, it should prompt you to set various `DMARC` configs to prevent email spoofing 17 | * There should be a `Review Settings` button on the `Website Overview` dashboard, which points to a link like [this](https://dash.cloudflare.com/__WEBSITE_ID__/__MY_SITE__.__COM__/recommendations) 18 | -------------------------------------------------------------------------------- /docs/jellyfin_client_whitelist.conf: -------------------------------------------------------------------------------- 1 | access_by_lua_block { 2 | local cjson = require "cjson" 3 | local ip_cache_expiration_seconds = 86400 4 | 5 | function is_active_authentik_user_ip(client_ip) 6 | local ip_cache = ngx.shared.authentik_ip_cache 7 | local is_ip_cached, _flags = ip_cache:get(client_ip) 8 | if is_ip_cached then 9 | return true 10 | elseif is_active_authentik_session(client_ip) then 11 | ip_cache:flush_expired() 12 | success, err, _forcible = ip_cache:set(client_ip, true, ip_cache_expiration_seconds) 13 | if err then 14 | ngx.log(ngx.ERR, "is_active_authentik_user_ip error: ", err) 15 | error(err) 16 | end 17 | return true 18 | else 19 | return false 20 | end 21 | end 22 | 23 | function is_active_authentik_session(client_ip) 24 | ngx.req.read_body() 25 | local res = ngx.location.capture( 26 | "/authentik_sessions/", 27 | { 28 | method = ngx.HTTP_GET, 29 | args = { 30 | last_ip = client_ip, 31 | ordering = "expires desc", 32 | page_size = 1 33 | } 34 | } 35 | ) 36 | if res.status ~= 200 then 37 | ngx.log(ngx.ERR, "is_active_authentik_session res.status: ", res.status) 38 | return false 39 | end 40 | 41 | local json_res = cjson.decode(res.body) 42 | if json_res.pagination.count == 0 then 43 | return false 44 | end 45 | 46 | local session = json_res.results[1] 47 | local expiration_time = parse_iso8601(session.expires) 48 | return os.time() < expiration_time 49 | end 50 | 51 | function parse_iso8601(stamp) 52 | local groups, err = ngx.re.match( 53 | stamp, 54 | [[^(?[0-9]{4})-(?[0-9]{1,2})-(?[0-9]{1,2})T(?[0-9]{1,2}):(?[0-9]{1,2}):(?[0-9]{1,2})(?:\.[0-9]+)?Z$]] 55 | ) 56 | if err or (groups == nil) then 57 | ngx.log(ngx.ERR, "parse_iso8601 err: " .. err .. " groups: " .. groups) 58 | error(err) 59 | end 60 | return os.time(groups) 61 | end 62 | 63 | if not is_active_authentik_user_ip(ngx.var.remote_addr) then 64 | ngx.exit(ngx.HTTP_FORBIDDEN) 65 | end 66 | } 67 | -------------------------------------------------------------------------------- /docs/jellyfin_client_whitelist.md: -------------------------------------------------------------------------------- 1 | # Jellyfin Dynamic IP Whitelist 2 | 3 | 4 | ### Background 5 | 6 | Currently, only web browsers are capable of accessing 7 | a Jellyfin server behind Authentik. 8 | 9 | See examples [here](https://github.com/jellyfin/jellyfin-android/issues/123), 10 | [here](https://features.jellyfin.org/posts/471/header-authentication), 11 | & [here](https://features.jellyfin.org/posts/1461/capability-to-specify-client-certificate-for-android-client). 12 | 13 | Following below is a work-around for the lack of authentication 14 | mechanisms supported by the various Jellyfin [clients](https://jellyfin.org/clients/) 15 | (Android, etc.). 16 | 17 | The end result is that the Jellyfin server will be accessible 18 | via any of the client apps, while still not exposed to the 19 | internet at large. 20 | 21 | 22 | You may recall that our current setup flows like this: 23 | 24 | 👤 -> `VPS` -> `Nginx` -> `Tailscale` -> `Nginx` -> `Authentik` -> `Jellyfin Login Screen` 25 | 26 | The work-around implemented here instead does this: 27 | 28 | 👤 -> `VPS` -> `Nginx` -> `Tailscale` -> `{Nginx IP Check}` -> `Jellyfin Login Screen` 29 | 30 | 31 | The logic of the `{Nginx IP Check}` works like this: 32 | 33 | 1) Nginx directly queries Authentik via API to 34 | check if the client IP address is associated 35 | with an unexpired Authentik session 36 | * (Authentik sessions expire after 2 weeks) 37 | 1) If the check passes, then Nginx proxies 38 | directly to Jellyfin, bypassing Authentik 39 | * (Nginx caches allowed IP addresses for 24 hours) 40 | 41 | --- 42 | 43 | ### Create an Authentik API Token 44 | 45 | * Open Authentik Admin web ui 46 | * Click `Directory` > `Tokens & App Passwords` > `Create` 47 | * `Identifier` = `npm-ip-whitelist` 48 | * `User` = `akadmin` 49 | * `Intent` = `API Token` 50 | * Disable `Expiration` 51 | * Click the 📋 button to copy the `npm-ip-whitelist` token 52 | * (We'll use this below) 53 | 54 | 55 | ### Configure Nginx Proxy Manager 56 | 57 | (Use Nginx Proxy Manager running on your local 58 | network, *not* on your VPS). 59 | 60 | * Open NPM and click `Add Proxy Host` 61 | * Click the `Details` tab 62 | * `Domain Names` = `jellyfin-whitelist.__MY_SITE__.__COM__` 63 | * `Scheme` = `http` 64 | * `Hostname` = `does-not-exist` 65 | * (we'll configure this elsewhere) 66 | * `Port` = `4321` 67 | * (we'll configure this elsewhere) 68 | * Enable `Block Common Exploits` 69 | * Enable `Websockets Support` 70 | * Click the `Custom locations` tab 71 | * Click `Add location` 72 | * `location` = `/` 73 | * `Scheme` = `http` 74 | * `Hostname` = `__JELLYFIN_IP__` 75 | * `Port` = `__JELLYFIN_PORT__` 76 | * Click the ⚙️ button to enter your custom Nginx configuration: 77 | * Paste the contents of [./jellyfin_client_whitelist.conf](./jellyfin_client_whitelist.conf) 78 | * Click `Add location` 79 | * `location` = `/authentik_sessions/` 80 | * `Scheme` = `http` 81 | * `Hostname` = `authentik-server/api/v3/core/authenticated_sessions/` 82 | * `Port` = `9000` 83 | * Click the ⚙️ button to enter your custom Nginx configuration: 84 | ```nginx 85 | internal; 86 | proxy_pass_request_headers off; 87 | proxy_pass_request_body off; 88 | proxy_set_header Accept "application/json"; 89 | proxy_set_header Authorization "Bearer __Authentik_API_Token__"; 90 | ``` 91 | * (Replace `__Authentik_API_Token__` with the value from prior) 92 | * `Save` the proxy 93 | 94 | 95 | ### Test the Whitelist 96 | 97 | * Failure/Block Mode 98 | * Try to access `https://jellyfin-whitelist.__MY_SITE__.__COM__` from a new IP address 99 | * (Use your phone, vpn, etc.) 100 | * You should get a `403 Forbidden` error 101 | * Success/Allow Mode 102 | * Login to `https://auth.__MY_SITE__.__COM__` 103 | * Now visit `https://jellyfin-whitelist.__MY_SITE__.__COM__` 104 | * You should get the Jellyfin login page 105 | 106 | --- 107 | 108 | In order to use the Jellyfin clients: 109 | 110 | 1) Login to `https://auth.__MY_SITE__.__COM__` from a web browser 111 | 1) Use `https://jellyfin-whitelist.__MY_SITE__.__COM__:443` from your app 112 | 113 | ☝️ do (1) and (2) from the same IP address 114 | -------------------------------------------------------------------------------- /docs/jellyfin_ldap.md: -------------------------------------------------------------------------------- 1 | # Configure Jellyfin for LDAP Authentication 2 | 3 | 1) Open the Authentik Admin Dashboard 4 | * Go to `Directory` > `Users` 5 | * Click `📁 Root` 6 | * Click `>` to expand user `service-ldap-outpost` 7 | * Click `Set Password` 8 | * You can use `openssl rand -base64 36` to generate a password 9 | 1) Install the [Jellyfin LDAP-Auth Plugin](https://github.com/jellyfin/jellyfin-plugin-ldapauth#installation) 10 | 1) Configure the plugin's settings via Jellyfin UI 11 | * `LDAP Server` = `__HOST_IP__` 12 | * `Secure LDAP` = false 13 | * `LDAP Base DN for searches` = `dc=ldap,dc=__MY_SITE__,dc=__COM__` 14 | * `LDAP Port` = `389` 15 | * `LDAP Attributes` = `cn` 16 | * `LDAP Name Attribute` = `cn` 17 | * `LDAP User Filter` = `(memberOf=cn=jellyfin-users,ou=groups,dc=ldap,dc=__MY_SITE__,dc=__COM__)` 18 | * `LDAP Admin Filter` = `(memberOf=cn=jellyfin-admins,ou=groups,dc=ldap,dc=__MY_SITE__,dc=__COM__)` 19 | * `LDAP Bind User` = `cn=service-ldap-outpost,ou=users,dc=ldap,dc=__MY_SITE__,dc=__COM__` 20 | * `LDAP Bind User Password` = `__PASSWORD_FROM_ABOVE__` 21 | * `Enable Case Insensitive Username` = true 22 | -------------------------------------------------------------------------------- /docs/jellyfin_ldap_users.md: -------------------------------------------------------------------------------- 1 | # Create Jellyfin Users via Authentik 2 | 3 | 1) Open the Authentik Admin Dashboard 4 | * Go to `Directory` > `Users` 5 | * Click `Create` 6 | * `Username` = `` 7 | * `Groups` = `jellyfin-users` 8 | * Click `>` to expand user `` 9 | * Click `Set password` 10 | * Configure the user's password 11 | 1) You should now be able to login to Jellyfin with the above user via [`https://jf.__MY_SITE__.__COM__`](https://jf.__MY_SITE__.__COM__) 12 | * (note that with caching enabled on the LDAP Provider, it may take up to 5 minutes for the refresh to load a new user) 13 | -------------------------------------------------------------------------------- /docs/npm_to_authentik.md: -------------------------------------------------------------------------------- 1 | # Configure Nginx -> Authentik 2 | 3 | (Use Nginx Proxy Manager running on your local 4 | network, *not* on your VPS). 5 | 6 | * Open NPM and click `Add Proxy Host` 7 | * Under the `Details` tab, set these values: 8 | * `Domain Names` = `*.__MY_SITE__.__COM__` 9 | * `Scheme` = `http` 10 | * `Forward Hostname` = `authentik-server` 11 | * `Forward Port` = `9000` 12 | * enable `Block Common Exploits` 13 | * enable `Websockets Support` 14 | * You should now be able to access Authentik via [`https://auth.__MY_SITE__.__COM__`](https://auth.__MY_SITE__.__COM__) 15 | -------------------------------------------------------------------------------- /docs/tailscale_acl.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "groups": { 3 | "group:tailscale-admin": ["__YOUR_EMAIL__"], 4 | }, 5 | "tagOwners": { 6 | "tag:vps-tunnel": ["group:tailscale-admin"], 7 | "tag:npm-tunnel": ["group:tailscale-admin"], 8 | }, 9 | "acls": [ 10 | { 11 | "action": "accept", 12 | "src": ["group:tailscale-admin"], 13 | "dst": ["*:*"], 14 | }, 15 | { 16 | "action": "accept", 17 | "src": ["tag:vps-tunnel"], 18 | "dst": ["tag:npm-tunnel:80"], 19 | }, 20 | { 21 | "action": "accept", 22 | "src": ["tag:npm-tunnel"], 23 | "dst": ["tag:vps-tunnel:80"], 24 | }, 25 | ], 26 | "tests": [ 27 | { // Accept admin 28 | "src": "group:tailscale-admin", 29 | "accept": ["tag:npm-tunnel:123"], 30 | }, 31 | { // Accept admin 32 | "src": "group:tailscale-admin", 33 | "accept": ["tag:vps-tunnel:456"], 34 | }, 35 | { // Accept http VPS Tunnel -> NPM Tunnel 36 | "src": "tag:vps-tunnel", 37 | "accept": ["tag:npm-tunnel:80"], 38 | }, 39 | { // Accept http NPM Tunnel -> VPS Tunnel 40 | "src": "tag:npm-tunnel", 41 | "accept": ["tag:vps-tunnel:80"], 42 | }, 43 | { // Deny non-http VPS Tunnel -> NPM Tunnel 44 | "src": "tag:vps-tunnel", 45 | "deny": ["tag:npm-tunnel:123"], 46 | }, 47 | { // Deny non-http NPM Tunnel -> VPS Tunnel 48 | "src": "tag:npm-tunnel", 49 | "deny": ["tag:vps-tunnel:456"], 50 | }, 51 | { // Deny VPS Tunnel -> Admin 52 | "src": "tag:vps-tunnel", 53 | "deny": ["group:tailscale-admin:80"], 54 | }, 55 | { // Deny NPM Tunnel -> Admin 56 | "src": "tag:npm-tunnel", 57 | "deny": ["group:tailscale-admin:80"], 58 | }, 59 | ], 60 | } 61 | -------------------------------------------------------------------------------- /docs/tailscale_configure.md: -------------------------------------------------------------------------------- 1 | # Configure Tailscale 2 | 3 | * [Register](https://login.tailscale.com/start) for a Tailscale Account 4 | * `Enable MagicDNS` for Tailscale [here](https://login.tailscale.com/admin/dns) 5 | * Modify Tailscale ACL [here](https://login.tailscale.com/admin/acls) 6 | * Replace `__YOUR_EMAIL__` in [./tailscale_acl.jsonc](./tailscale_acl.jsonc) 7 | -------------------------------------------------------------------------------- /docs/vps_harden.md: -------------------------------------------------------------------------------- 1 | # Harden Your VPS 2 | 3 | Unless otherwise noted, all commands are to be run on your VPS. 4 | 5 | Your VPS provider should have given you credentials to SSH to your VPS, for example: 6 | 7 | ```shell 8 | ssh "root@$VPS_IP" 9 | # 10 | ``` 11 | 12 | 13 | ### Create Non-Root User 14 | 15 | ```shell 16 | YOUR_USER='' 17 | useradd -m -s /bin/bash "$YOUR_USER" # create user 18 | passwd "$YOUR_USER" # set password 19 | usermod -aG sudo "$YOUR_USER" # add to sudo 20 | id "$YOUR_USER" # should show sudo membership 21 | which sudo || apt install sudo # ensure sudo package install 22 | su "$YOUR_USER" # use this user going forward 23 | ``` 24 | 25 | 26 | ### Configure User SSH Key 27 | 28 | Run these commands on your local machine 29 | 30 | ```shell 31 | KEY_NAME='' 32 | VPS_IP='' 33 | 34 | # create your public+private key pair 35 | ssh-keygen -t rsa -b 4096 -o -a 100 -f ~/.ssh/"$KEY_NAME" 36 | 37 | # ensure keys are only read-writable by $USER 38 | chmod 600 ~/.ssh/"$KEY_NAME" ~/.ssh/"$KEY_NAME".pub 39 | 40 | # install public key on VPS 41 | ssh-copy-id -i ~/.ssh/"$KEY_NAME".pub "$YOUR_USER@$VPS_IP" 42 | 43 | # test login with private key 44 | ssh -i ~/.ssh/"$KEY_NAME" "$YOUR_USER@$VPS_IP" 45 | ``` 46 | 47 | 48 | ### Configure SSH Non-Standard Port 49 | 50 | ```shell 51 | # get a random port 52 | # https://unix.stackexchange.com/a/423052 53 | SSH_RAND_PORT=`comm -23 <(seq 49152 65535 | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n 1` 54 | echo "SSH_RAND_PORT = $SSH_RAND_PORT" 55 | 56 | # update /etc/ssh/sshd_config 57 | sudo sed -i.bak "s/^#Port 22$/Port $SSH_RAND_PORT/" /etc/ssh/sshd_config 58 | 59 | # review changes 60 | diff /etc/ssh/sshd_config /etc/ssh/sshd_config.bak 61 | 62 | # reboot with changes 63 | sudo systemctl restart sshd 64 | 65 | # confirm reboot 66 | systemctl status sshd 67 | 68 | # from a shell on your local machine, confirm that you can connect via new port 69 | ssh -i ~/.ssh/"$KEY_NAME" -p "$SSH_RAND_PORT" "$YOUR_USER@$VPS_IP" 70 | 71 | # remove backup 72 | sudo rm /etc/ssh/sshd_config.bak 73 | ``` 74 | 75 | 76 | ### Configure SSH Client 77 | 78 | Run these commands on your local machine to append to `~/.ssh/config` 79 | 80 | ```shell 81 | VPS_SSH_NAME='vps' 82 | 83 | cat << EOF >> ~/.ssh/config 84 | Host $VPS_SSH_NAME 85 | Hostname $VPS_IP 86 | Port $SSH_RAND_PORT 87 | User $YOUR_USER 88 | IdentityFile ~/.ssh/"$KEY_NAME" 89 | EOF 90 | 91 | # test that you can SSH via config 92 | ssh "$VPS_SSH_NAME" 93 | ``` 94 | 95 | 96 | ### Limit SSH Login Methods 97 | 98 | We are going to config the following in `/etc/ssh/sshd_config`: 99 | 100 | ``` 101 | ChallengeResponseAuthentication no 102 | PasswordAuthentication no 103 | UsePAM no 104 | PermitRootLogin no 105 | PermitRootLogin prohibit-password 106 | ``` 107 | 108 | After this, you will only be able to SSH using (1) a key and (2) a non-root user. 109 | 110 | ```shell 111 | # update /etc/ssh/sshd_config 112 | new_line=$'\n' 113 | sudo sed -i.bak -r \ 114 | -e 's/^#?ChallengeResponseAuthentication (yes|no)/ChallengeResponseAuthentication no/' \ 115 | -e 's/^#?PasswordAuthentication (yes|no)/PasswordAuthentication no/' \ 116 | -e 's/^#?UsePAM (yes|no)/UsePAM no/' \ 117 | -e "s/^#?PermitRootLogin (yes|no)/PermitRootLogin no\\${new_line}PermitRootLogin prohibit-password/" \ 118 | /etc/ssh/sshd_config 119 | 120 | # review changes 121 | diff /etc/ssh/sshd_config /etc/ssh/sshd_config.bak 122 | 123 | # reboot with changes 124 | sudo systemctl restart sshd 125 | 126 | # confirm reboot 127 | systemctl status sshd 128 | 129 | # using your local machine, confirm that you can still connect 130 | ssh "$VPS_SSH_NAME" 131 | 132 | # remove backup 133 | sudo rm /etc/ssh/sshd_config.bak 134 | ``` 135 | 136 | Run these commands on your local machine 137 | 138 | ```shell 139 | # check that root is blocked 140 | ssh -p "$SSH_RAND_PORT" "root@$VPS_IP" 141 | 142 | # check that password is blocked 143 | ssh -p "$SSH_RAND_PORT" "$YOUR_USER@$VPS_IP" -o 'PubkeyAuthentication=no' 144 | ``` 145 | 146 | 147 | ### Disable SSH v1 Protocol 148 | 149 | ```shell 150 | # backup 151 | sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak 152 | 153 | # append protocol config 154 | sudo tee -a /etc/ssh/sshd_config </dev/null 155 | 156 | # Only allow SSH2 157 | Protocol 2 158 | EOF 159 | 160 | # review changes 161 | diff /etc/ssh/sshd_config /etc/ssh/sshd_config.bak 162 | 163 | # reboot with changes 164 | sudo systemctl restart sshd 165 | 166 | # confirm reboot 167 | systemctl status sshd 168 | 169 | # using your local machine, confirm that you can still connect 170 | ssh "$VPS_SSH_NAME" 171 | 172 | # remove backup 173 | sudo rm /etc/ssh/sshd_config.bak 174 | ``` 175 | -------------------------------------------------------------------------------- /docs/vps_routing.md: -------------------------------------------------------------------------------- 1 | # Route 👤 -> `VPS` -> `Nginx` -> `Tailscale` -> `Nginx` 2 | 3 | 4 | ### Configure `Nginx` -> `Tailscale` -> `Nginx` 5 | 6 | Reminder for how to access Nginx Proxy Manager on the VPS: 7 | * Copy the value of your `Tailnet name` from [here](https://login.tailscale.com/admin/dns) 8 | * The admin gui is available at [`http://vps-tunnel.__TAILNET_NAME__:81`](http://vps-tunnel.__TAILNET_NAME__:81) 9 | 10 | --- 11 | 12 | 1) Create Cloudflare API Token 13 | * Open the API Token dashboard [here](https://dash.cloudflare.com/profile/api-tokens) 14 | * Click `Create Token` 15 | * Click `Use template` for `Edit zone DNS` 16 | * Under `Zone Resources` select `__MY_SITE__.__COM__` 17 | * Under `Client IP Address Filtering` set `Is in` = `__VPS_IP__` 18 | * Click `Continue to Summary` > `Create Token` 19 | * Copy and record the token 20 | 1) Add SSL Certificate to Nginx Proxy Manager on the VPS 21 | * Open the `SSL Certificates` tab in NPM 22 | * Click `Add SSL Certificate` 23 | * `Domain Names` = `*.__MY_SITE__.__COM__` and `__MY_SITE__.__COM__` 24 | * Enable `Use a DNS Challenge` 25 | * `DNS Provider` = `Cloudflare` 26 | * Update `Credentials File Content` with your Cloudflare API token from above 27 | * Click `Save` 28 | 1) Define Reverse Proxy for Nginx Proxy Manager on the VPS 29 | * Open the `Hosts` > `Proxy Hosts` tab in NPM 30 | * Click `Add Proxy Host` 31 | * Under the `Details` tab 32 | * `Domain Names` = `*.__MY_SITE__.__COM__` 33 | * `Scheme` = `http` 34 | * `Forward Host` = `__NPM-TUNNEL_IP__` 35 | * (Acquire the above IP from [here](https://login.tailscale.com/admin/machines)) 36 | * `Port` = `80` 37 | * Enable `Block Common Exploits` 38 | * Enable `Websockets Support` 39 | * Under the `SSL` tab 40 | * `SSL Certificate` = *the certificate you created above* 41 | * Enable `Force SSL` 42 | * Enable `HTTP/2 Support` 43 | * Click `Save` 44 | 1) Define Redirection Host for Nginx Proxy Manager on the VPS 45 | * Open the `Hosts` > `Redirection Hosts` tab in NPM 46 | * Click `Add Redirection Host` 47 | * Under the `Details` tab 48 | * `Domain Names` = `__MY_SITE__.__COM__` 49 | * `Scheme` = `https` 50 | * `Forward Domain` = `auth.__MY_SITE__.__COM__` 51 | * `HTTP Code` = `301` 52 | * Enable `Preserve Path` 53 | * Enable `Block Common Exploits` 54 | * Under the `SSL` tab 55 | * `SSL Certificate` = *the certificate you created above* 56 | * Enable `Force SSL` 57 | * Enable `HTTP/2 Support` 58 | * Click `Save` 59 | 60 | 61 | ### Configure Cloudflare DNS -> `VPS` 62 | 63 | * Login to the Cloudflare dashboard [here](https://dash.cloudflare.com) 64 | * Click through to `Websites` > `__MY_SITE__.__COM__` > `DNS` 65 | * Click `Edit` for `CNAME` of `__MY_SITE__.__COM__` 66 | * `Delete` the record 67 | * Click `Add Record` 68 | * `Type` = `A` 69 | * `Name` = `__MY_SITE__.__COM__` 70 | * `IPv4 address` = `__VPS_IP__` 71 | * `Proxy status` = Disabled 72 | -------------------------------------------------------------------------------- /nginx_proxy_manager/README.md: -------------------------------------------------------------------------------- 1 | This was modified from the NPM [docs](https://nginxproxymanager.com/setup/#running-the-app) 2 | 3 | * Copy the variables template via `cp ./template.env ./.env` 4 | * Open `./.env` and set the SECRET values 5 | * Deploy via `docker-compose up -d` 6 | 7 | --- 8 | 9 | After deploying, you can access the admin gui via [`http://__HOST_IP__:81`](http://__HOST_IP__:81) 10 | 11 | --- 12 | 13 | Login to the gui with the below and reset your password: 14 | ```yml 15 | Email: admin@example.com 16 | Password: changeme 17 | ``` 18 | 19 | ☝️ Very important -- don't skip that step ⚠️ 20 | -------------------------------------------------------------------------------- /nginx_proxy_manager/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3" 3 | 4 | services: 5 | nginx-proxy-manager: 6 | image: jc21/nginx-proxy-manager:$NPM_TAG 7 | restart: unless-stopped 8 | healthcheck: 9 | test: ["CMD", "/bin/check-health"] 10 | interval: 10s 11 | timeout: 3s 12 | depends_on: 13 | - npm-mysql 14 | environment: 15 | DB_MYSQL_HOST: npm-mysql 16 | DB_MYSQL_NAME: $NPM__MYSQL_DB 17 | DB_MYSQL_PASSWORD: $NPM__MYSQL_PASS 18 | DB_MYSQL_PORT: 3306 19 | DB_MYSQL_USER: $NPM__MYSQL_USER 20 | # Uncomment if IPv6 is not enabled on your host 21 | # DISABLE_IPV6: true 22 | volumes: 23 | - ./mounts/data:/data 24 | - npm_letsencrypt:/etc/letsencrypt 25 | ports: 26 | - 80:80 27 | - 443:443 28 | - 81:81 29 | networks: 30 | - authentik 31 | - default 32 | npm-mysql: 33 | image: yobasystems/alpine-mariadb:10.6 34 | restart: unless-stopped 35 | environment: 36 | MYSQL_DATABASE: $NPM__MYSQL_DB 37 | MYSQL_PASSWORD: $NPM__MYSQL_PASS 38 | MYSQL_ROOT_PASSWORD: $NPM__MYSQL_ROOT_PASS 39 | MYSQL_USER: $NPM__MYSQL_USER 40 | volumes: 41 | - npm_database:/var/lib/mysql 42 | volumes: 43 | npm_database: 44 | driver: local 45 | npm_letsencrypt: 46 | driver: local 47 | networks: 48 | authentik: 49 | name: authentik 50 | -------------------------------------------------------------------------------- /nginx_proxy_manager/mounts/data/nginx/custom/README.md: -------------------------------------------------------------------------------- 1 | see the [docs](https://nginxproxymanager.com/advanced-config/#custom-nginx-configurations) 2 | -------------------------------------------------------------------------------- /nginx_proxy_manager/mounts/data/nginx/custom/http_top.conf: -------------------------------------------------------------------------------- 1 | lua_shared_dict authentik_ip_cache 1m; 2 | -------------------------------------------------------------------------------- /nginx_proxy_manager/mounts/data/nginx/custom/server_proxy.conf: -------------------------------------------------------------------------------- 1 | set_real_ip_from 100.64.0.0/10; # https://tailscale.com/kb/1015/100.x-addresses/ 2 | real_ip_header X-Real-IP; 3 | real_ip_recursive on; 4 | -------------------------------------------------------------------------------- /nginx_proxy_manager/template.env: -------------------------------------------------------------------------------- 1 | # replace SECRET with any random values 2 | # e.g. `openssl rand -base64 36` 3 | 4 | NPM__MYSQL_DB=npm_db 5 | NPM__MYSQL_PASS=SECRET 6 | NPM__MYSQL_ROOT_PASS=SECRET 7 | NPM__MYSQL_USER=npm_user 8 | NPM_TAG=latest # NPM_TAG=2.9.14 at time of writing 9 | -------------------------------------------------------------------------------- /npm_tunnel/README.md: -------------------------------------------------------------------------------- 1 | # Deploy Tailscale 2 | 3 | 4 | 1) Config `./.env` 5 | * Copy the variables template via `cp ./template.env ./.env` 6 | 1) Authenticate Tailscale Container 7 | * Execute `docker-compose run --rm tailscale /bin/sh /tailscale/run.sh` 8 | * Copy the URL from the above command's output 9 | * Paste the URL into your web browser 10 | * Authorize the container 11 | * The container's output should update with an eventual `health("overall"): ok` 12 | * `Ctrl-C` to kill the container 13 | 1) Disable Key Expiry for `npm-tunnel` machine 14 | * Via the [docs](https://tailscale.com/kb/1028/key-expiry/#disabling-key-expiry) 15 | 1) Deploy the Stack 16 | * Deploy via `docker-compose up -d` 17 | * Check tailscale status: 18 | ```shell 19 | docker exec -it $(docker container ls | grep tailscale | awk '{print $1}') tailscale status 20 | ``` 21 | -------------------------------------------------------------------------------- /npm_tunnel/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3" 3 | 4 | services: 5 | tailscale: 6 | privileged: true 7 | network_mode: host 8 | cap_add: 9 | - net_admin 10 | - sys_module 11 | image: tailscale/tailscale:$TAILSCALE_TAG 12 | restart: unless-stopped 13 | hostname: npm-tunnel 14 | command: tailscaled --statedir=/var/lib/tailscale_state 15 | environment: 16 | TS_EXTRA_ARGS: --advertise-tags=tag:npm-tunnel 17 | TS_SOCKET: /var/run/tailscale/tailscaled.sock 18 | TS_STATE_DIR: /var/lib/tailscale_state 19 | volumes: 20 | - tailscale_state:/var/lib/tailscale_state 21 | - /dev/net/tun:/dev/net/tun 22 | volumes: 23 | tailscale_state: 24 | driver: local 25 | -------------------------------------------------------------------------------- /npm_tunnel/template.env: -------------------------------------------------------------------------------- 1 | TAILSCALE_TAG=stable # TAILSCALE_TAG=v1.32.0 at time of writing 2 | -------------------------------------------------------------------------------- /vps_tunnel/README.md: -------------------------------------------------------------------------------- 1 | # Deploy VPS Tunnel Stack 2 | 3 | 4 | ### Prerequisites 5 | 6 | * Install [docker-compose](../docker_install.sh) on your VPS 7 | 8 | 9 | ### Deploy Tunnel on VPS 10 | 11 | 1) Config `./.env` Secrets 12 | * Copy the variables template via `cp ./template.env ./.env` 13 | * Open `./.env` and set the SECRET values 14 | 1) Authenticate Tailscale Container 15 | * Execute `docker-compose run --rm tailscale /bin/sh /tailscale/run.sh` 16 | * Copy the URL from the above command's output 17 | * Paste the URL into your web browser 18 | * Authorize the container 19 | * The container's output should update with an eventual `health("overall"): ok` 20 | * `Ctrl-C` to kill the container 21 | 1) Disable Key Expiry for `vps-tunnel` machine 22 | * Via the [docs](https://tailscale.com/kb/1028/key-expiry/#disabling-key-expiry) 23 | 1) Deploy the Stack 24 | * Deploy via `docker-compose up -d` 25 | * Check the stack's health via `docker-compose ps` 26 | * All 3 containers should eventually have `State` = `Up` 27 | * Check tailscale status: 28 | ```shell 29 | docker exec -it $(docker container ls | grep tailscale | awk '{print $1}') tailscale status 30 | ``` 31 | 32 | 33 | ### Configure NPM Admin 34 | 35 | * Ensure you have a [Tailscale client](https://tailscale.com/download) running on your local machine 36 | * Copy the value of your `Tailnet name` from [here](https://login.tailscale.com/admin/dns) 37 | * Open the Nginx Proxy Manager admin gui via [`http://vps-tunnel.__TAILNET_NAME__:81`](http://vps-tunnel.__TAILNET_NAME__:81) 38 | * Login to the gui with the below and reset your password: 39 | ```yml 40 | Email: admin@example.com 41 | Password: changeme 42 | ``` 43 | ☝️ Very important -- don't skip that step ⚠️ 44 | -------------------------------------------------------------------------------- /vps_tunnel/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | tailscale: 4 | privileged: true 5 | network_mode: host 6 | cap_add: 7 | - net_admin 8 | - sys_module 9 | image: tailscale/tailscale:$TAILSCALE_TAG 10 | restart: unless-stopped 11 | hostname: vps-tunnel 12 | command: tailscaled --statedir=/var/lib/tailscale_state 13 | environment: 14 | TS_EXTRA_ARGS: --advertise-tags=tag:vps-tunnel 15 | TS_SOCKET: /var/run/tailscale/tailscaled.sock 16 | TS_STATE_DIR: /var/lib/tailscale_state 17 | volumes: 18 | - tailscale_state:/var/lib/tailscale_state 19 | - /dev/net/tun:/dev/net/tun 20 | nginx-proxy-manager: 21 | image: jc21/nginx-proxy-manager:$NPM_TAG 22 | restart: unless-stopped 23 | depends_on: 24 | - npm-mysql 25 | - tailscale 26 | healthcheck: 27 | test: ["CMD", "/bin/check-health"] 28 | interval: 10s 29 | timeout: 3s 30 | environment: 31 | DB_MYSQL_HOST: npm-mysql 32 | DB_MYSQL_NAME: $NPM__MYSQL_DB 33 | DB_MYSQL_PASSWORD: $NPM__MYSQL_PASS 34 | DB_MYSQL_PORT: 3306 35 | DB_MYSQL_USER: $NPM__MYSQL_USER 36 | # Uncomment if IPv6 is not enabled on your host 37 | # DISABLE_IPV6: true 38 | volumes: 39 | - ./mounts/data:/data 40 | - npm_letsencrypt:/etc/letsencrypt 41 | ports: 42 | - 80:80 43 | - 443:443 44 | - 81:81 45 | npm-mysql: 46 | image: yobasystems/alpine-mariadb:10.6 47 | restart: unless-stopped 48 | environment: 49 | MYSQL_DATABASE: $NPM__MYSQL_DB 50 | MYSQL_PASSWORD: $NPM__MYSQL_PASS 51 | MYSQL_ROOT_PASSWORD: $NPM__MYSQL_ROOT_PASS 52 | MYSQL_USER: $NPM__MYSQL_USER 53 | volumes: 54 | - npm_database:/var/lib/mysql 55 | volumes: 56 | npm_database: 57 | driver: local 58 | npm_letsencrypt: 59 | driver: local 60 | tailscale_state: 61 | driver: local 62 | -------------------------------------------------------------------------------- /vps_tunnel/mounts/data/nginx/custom/README.md: -------------------------------------------------------------------------------- 1 | see the [docs](https://nginxproxymanager.com/advanced-config/#custom-nginx-configurations) 2 | -------------------------------------------------------------------------------- /vps_tunnel/mounts/data/nginx/custom/server_proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_set_header X-Real-IP $remote_addr; 2 | -------------------------------------------------------------------------------- /vps_tunnel/template.env: -------------------------------------------------------------------------------- 1 | # replace NPM__MYSQL_* SECRET with any random values 2 | # e.g. `openssl rand -base64 36` 3 | 4 | NPM__MYSQL_DB=npm_db 5 | NPM__MYSQL_PASS=SECRET 6 | NPM__MYSQL_ROOT_PASS=SECRET 7 | NPM__MYSQL_USER=npm_user 8 | NPM_TAG=latest # NPM_TAG=2.9.14 at time of writing 9 | TAILSCALE_TAG=stable # TAILSCALE_TAG=v1.32.0 at time of writing 10 | --------------------------------------------------------------------------------