From f2948074d98594e63a4b3732692a5cb90b49a98f Mon Sep 17 00:00:00 2001 From: Matthew Pomes Date: Mon, 1 Sep 2025 19:01:24 -0500 Subject: [PATCH] Current state --- .gitignore | 1 + authelia/configuration.yml | 54 +++++++++ authelia/gen-secrets.sh | 10 ++ build-certs.sh | 9 ++ docker-compose.yaml | 84 ++++++++++---- jellyfin-compose.yaml | 137 +++++++++++++++++++++++ nginx.dockerfile | 4 - nginx/sites-available/5d-diplomacy | 10 +- nginx/sites-enabled/auth | 62 ++++++++++ nginx/sites-enabled/jellyfin | 22 +++- nginx/sites-enabled/jellyseerr | 62 ++++++++++ nginx/sites-enabled/karakeep | 22 +++- nginx/sites-enabled/ollama | 42 +------ nginx/sites-enabled/primary | 19 +++- nginx/sites-enabled/servarr | 76 +++++++++++++ nginx/snippets/authelia-authrequest.conf | 32 ++++++ nginx/snippets/authelia-location.conf | 32 ++++++ nginx/snippets/letsencrypt.conf | 25 +++++ nginx/snippets/proxy.conf | 34 ++++++ nginx/snippets/websockets.conf | 3 + 20 files changed, 661 insertions(+), 79 deletions(-) create mode 100644 authelia/configuration.yml create mode 100644 authelia/gen-secrets.sh create mode 100644 build-certs.sh create mode 100644 jellyfin-compose.yaml delete mode 100644 nginx.dockerfile create mode 100644 nginx/sites-enabled/auth create mode 100644 nginx/sites-enabled/jellyseerr create mode 100644 nginx/sites-enabled/servarr create mode 100644 nginx/snippets/authelia-authrequest.conf create mode 100644 nginx/snippets/authelia-location.conf create mode 100644 nginx/snippets/letsencrypt.conf create mode 100644 nginx/snippets/proxy.conf create mode 100644 nginx/snippets/websockets.conf diff --git a/.gitignore b/.gitignore index 4c49bd7..97a6e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .env +authelia/secrets/ diff --git a/authelia/configuration.yml b/authelia/configuration.yml new file mode 100644 index 0000000..b2326e7 --- /dev/null +++ b/authelia/configuration.yml @@ -0,0 +1,54 @@ +authentication_backend: + refresh_interval: '5m' + password_reset: + disable: false + custom_url: '' + password_change: + disable: false + file: + path: '/config/users.yml' + watch: false + search: + email: false + case_insensitive: false + password: + algorithm: 'argon2' + argon2: + variant: 'argon2id' + iterations: 3 + memory: 65536 + parallelism: 4 + key_length: 32 + salt_length: 16 +session: + name: 'authelia_session' + # same_site: 'lax' + inactivity: '5m' + expiration: '1h' + remember_me: '1M' + cookies: + - domain: 'loadingm.xyz' + authelia_url: 'https://auth.loadingm.xyz' + default_redirection_url: 'https://loadingm.xyz' + name: 'authelia_session' + same_site: 'lax' + inactivity: '5m' + expiration: '1h' + remember_me: '1d' +notifier: + disable_startup_check: false + filesystem: + filename: '/config/notification.txt' +storage: + local: + path: '/config/db.sqlite3' +access_control: + default_policy: deny + rules: + - domain: '*.loadingm.xyz' + policy: one_factor +server: + endpoints: + authz: + auth-request: + implementation: 'AuthRequest' diff --git a/authelia/gen-secrets.sh b/authelia/gen-secrets.sh new file mode 100644 index 0000000..0747807 --- /dev/null +++ b/authelia/gen-secrets.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +cd $parent_path +mkdir -p ./secrets +openssl rand -base64 60 > ./secrets/JWT_SECRET +openssl rand -base64 60 > ./secrets/SESSION_SECRET +openssl rand -base64 60 > ./secrets/STORAGE_PASSWORD +openssl rand -base64 60 > ./secrets/STORAGE_ENCRYPTION_KEY + diff --git a/build-certs.sh b/build-certs.sh new file mode 100644 index 0000000..28ea9d5 --- /dev/null +++ b/build-certs.sh @@ -0,0 +1,9 @@ + +docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ \ + -d loadingm.xyz \ + -d auth.loadingm.xyz \ + -d jellyfin.loadingm.xyz \ + -d jellyseerr.loadingm.xyz \ + -d servarr.loadingm.xyz \ + -d karakeep.loadingm.xyz \ + -d ollama.loadingm.xyz diff --git a/docker-compose.yaml b/docker-compose.yaml index 167dedd..b66a344 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,6 @@ include: - ./karakeep-compose.yaml + - ./jellyfin-compose.yaml services: web: image: "nginx" @@ -8,47 +9,86 @@ services: - 80:80 - 443:443 volumes: - - ./nginx:/etc/nginx - - /etc/letsencrypt:/etc/letsencrypt - - /data/site:/data/site + - ./nginx:/etc/nginx:ro + - /data/site:/data/site:ro + - /data/certbot/www/:/var/www/certbot/:ro + # - /etc/letsencrypt:/etc/letsencrypt:ro + - /data/certbot/conf:/etc/letsencrypt:ro networks: - karakeep - ollama - jellyfin + - jellyfin-int + - auth depends_on: - jellyfin - ollama-webui - karakeep-web - jellyfin: - image: "jellyfin/jellyfin" - restart: unless-stopped - environment: - - JELLYFIN_PublishedServerUrl=http://jellyfin.loadingm.xyz - volumes: - - /var/lib/jellyfin:/data - - /etc/jellyfin:/config - - /var/cache/jellyfin:/cache - - type: bind - source: /data/library/ - target: /data/library - read_only: true - networks: - - jellyfin + - authelia + - qbittorrent # Optional - extra fonts to be used during transcoding with subtitle burn-in # - type: bind # source: /usr/local/share/fonts/cu # target: /usr/local/share/fonts/custom # read_only: true + certbot: + image: certbot/certbot:latest + volumes: + - /data/certbot/www/:/var/www/certbot/:rw + - /data/certbot/conf/:/etc/letsencrypt/:rw + authelia: + container_name: 'authelia' + image: 'docker.io/authelia/authelia:latest' + restart: 'unless-stopped' + secrets: ['JWT_SECRET', 'SESSION_SECRET', 'STORAGE_PASSWORD', 'STORAGE_ENCRYPTION_KEY'] + networks: + - auth + environment: + AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: '/run/secrets/JWT_SECRET' + AUTHELIA_SESSION_SECRET_FILE: '/run/secrets/SESSION_SECRET' + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: '/run/secrets/STORAGE_ENCRYPTION_KEY' + volumes: + - './authelia/:/config' # webdav: # image: "" - # minecraft: - # image: "" + minecraft: + image: itzg/minecraft-server:latest + tty: true + stdin_open: true + ports: + - "25565:25565" + environment: + EULA: "TRUE" + TYPE: "FABRIC" + MEMORY: "2048M" + MOTD: "Loading server..." + LEVEL: "world" + USE_MEOWICE_FLAGS: "true" + DIFFICULTY: "3" + OPS: |- + 187eca31-2e33-4199-97e0-2286bf35f7f8 + ENABLE_WHITELIST: "true" + WHITELIST: |- + 187eca31-2e33-4199-97e0-2286bf35f7f8 + PAUSE_WHEN_EMPTY_SECONDS: "20" + ENABLE_ROLLING_LOGS: "true" + volumes: + - "/opt/minecraft:/data" # calibre: # image: "linuxserver/calibre-web" # 5d-diplomacy-frontend: # image: "" # 5d-diplomacy-backend: # image: "" +secrets: + JWT_SECRET: + file: './authelia/secrets/JWT_SECRET' + SESSION_SECRET: + file: './authelia/secrets/SESSION_SECRET' + STORAGE_PASSWORD: + file: './authelia/secrets/STORAGE_PASSWORD' + STORAGE_ENCRYPTION_KEY: + file: './authelia/secrets/STORAGE_ENCRYPTION_KEY' volumes: meilisearch: karakeep: @@ -63,3 +103,7 @@ networks: external: false jellyfin: external: false + jellyfin-int: + external: false + auth: + external: false diff --git a/jellyfin-compose.yaml b/jellyfin-compose.yaml new file mode 100644 index 0000000..5d489ca --- /dev/null +++ b/jellyfin-compose.yaml @@ -0,0 +1,137 @@ +services: + qbittorrent: + image: lscr.io/linuxserver/qbittorrent:latest + container_name: qbittorrent + environment: + - WEBUI_PORT=8080 + - PUID=0 + - PGID=0 + - TZ=${TZ} + # - DOCKER_MODS=ghcr.io/gabe565/linuxserver-mod-vuetorrent + volumes: + - /data/jellyfin:/data/jellyfin + - /data/jellyfin/configs/qbittorrent:/config + - /data/jellyfin/qbittorrent/downloads:/downloads + ports: + - 8080:8080 + - 6881:6881 + - 6881:6881/udp + networks: + - jellyfin-int + restart: unless-stopped + flaresolverr: + image: ghcr.io/flaresolverr/flaresolverr:latest + container_name: flaresolverr + environment: + - LOG_LEVEL=${LOG_LEVEL:-info} + - LOG_HTML=${LOG_HTML:-false} + - CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none} + - TZ=${TZ} + #- LANG=fr_FR + #- LANG=en_US + ports: + - 8191:8191 + networks: + - jellyfin-int + restart: unless-stopped + prowlarr: + image: lscr.io/linuxserver/prowlarr:latest + container_name: prowlarr + environment: + - PUID=0 + - PGID=0 + - TZ=${TZ} + volumes: + - /data/jellyfin/configs/prowlarr:/config + ports: + - 9696:9696 + networks: + - jellyfin-int + restart: unless-stopped + jackett: + image: lscr.io/linuxserver/jackett:latest + container_name: jackett + environment: + - PUID=0 + - PGID=0 + - TZ=${TZ} + volumes: + - /data/jellyfin/configs/jackett:/config + ports: + - 9117:9117 + networks: + - jellyfin-int + restart: unless-stopped + sonarr: + image: lscr.io/linuxserver/sonarr:latest + container_name: sonarr + environment: + - PUID=0 + - PGID=0 + - TZ=${TZ} + volumes: + - /data/jellyfin:/data/jellyfin + - /data/jellyfin/configs/sonarr:/config + - /data/jellyfin/sonarr/tv:/tv + - /data/jellyfin/qbittorrent/downloads:/downloads + ports: + - 8989:8989 + networks: + - jellyfin-int + restart: unless-stopped + radarr: + image: lscr.io/linuxserver/radarr:latest + container_name: radarr + environment: + - PUID=0 + - PGID=0 + - TZ=${TZ} + volumes: + - /data/jellyfin:/data/jellyfin + - /data/jellyfin/configs/radarr:/config + - /data/jellyfin/radarr/movies:/movies + - /data/jellyfin/qbittorrent/downloads:/downloads + ports: + - 7878:7878 + networks: + - jellyfin-int + restart: unless-stopped + jellyfin: + image: lscr.io/linuxserver/jellyfin:latest + container_name: jellyfin + environment: + - PUID=0 + - PGID=0 + - TZ=${TZ} + - NVIDIA_VISIBLE_DEVICES=all + ports: + - 8096:8096 + - 8920:8920 + - 7359:7359/udp + - 1900:1900/udp + networks: + - jellyfin + - jellyfin-int + volumes: + - /data/library:/data/library:ro + - /data/jellyfin:/data/jellyfin + - /data/jellyfin/configs/jellyfin:/config + - /data/jellyfin/jellyfin/cache:/cache + - /data/jellyfin/sonarr/tv:/data/tvshows + - /data/jellyfin/radarr/movies:/data/movies + - /data/jellyfin/qbittorrent/downloads:/data/media_downloads + restart: unless-stopped + jellyseerr: + image: fallenbagel/jellyseerr:latest + container_name: jellyseerr + environment: + - LOG_LEVEL=debug + - TZ=${TZ} + ports: + - 5055:5055 + volumes: + - /data/jellyfin/configs/jellyseerr:/app/config + restart: unless-stopped + networks: + - jellyfin + - jellyfin-int diff --git a/nginx.dockerfile b/nginx.dockerfile deleted file mode 100644 index 09c83a2..0000000 --- a/nginx.dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM nginx -COPY ./nginx/nginx.conf /etc/nginx/ -COPY ./nginx/mime.types /etc/nginx/ -COPY ./nginx/sites-available /etc/nginx/ diff --git a/nginx/sites-available/5d-diplomacy b/nginx/sites-available/5d-diplomacy index 88f9773..a5c522b 100644 --- a/nginx/sites-available/5d-diplomacy +++ b/nginx/sites-available/5d-diplomacy @@ -8,10 +8,14 @@ server { listen [::]:80; server_name 5d-diplomacy.loadingm.xyz; + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + # Uncomment to redirect HTTP to HTTPS - return 301 https://$host$request_uri; - - + location / { + return 301 https://$host$request_uri; + } } server { diff --git a/nginx/sites-enabled/auth b/nginx/sites-enabled/auth new file mode 100644 index 0000000..b434d94 --- /dev/null +++ b/nginx/sites-enabled/auth @@ -0,0 +1,62 @@ +server { + listen 80; + listen [::]:80; + server_name auth.loadingm.xyz; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Uncomment to redirect HTTP to HTTPS + location / { + return 301 https://$host$request_uri; + } +} + +server { + # Nginx versions 1.25+ + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name auth.loadingm.xyz; + + ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. + client_max_body_size 20M; + + ssl_certificate /etc/letsencrypt/live/loadingm.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/loadingm.xyz/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + ssl_trusted_certificate /etc/letsencrypt/live/loadingm.xyz/chain.pem; + + # Security / XSS Mitigation Headers + add_header X-Content-Type-Options "nosniff"; + + # Permissions policy. May cause issues with some clients + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always; + + # Content Security Policy + # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + # Enforces https content and restricts JS/CSS to origin + # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. + add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + # Proxy main karakeep traffic + proxy_pass http://authelia:9091; + proxy_set_header Host $host; + 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 X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + + # Disable buffering when the nginx proxy gets very resource heavy upon streaming + proxy_buffering off; + } +} diff --git a/nginx/sites-enabled/jellyfin b/nginx/sites-enabled/jellyfin index e65f94b..b45aac8 100644 --- a/nginx/sites-enabled/jellyfin +++ b/nginx/sites-enabled/jellyfin @@ -3,8 +3,14 @@ server { listen [::]:80; server_name jellyfin.loadingm.xyz; + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + # Uncomment to redirect HTTP to HTTPS - return 301 https://$host$request_uri; + location / { + return 301 https://$host$request_uri; + } } server { @@ -18,11 +24,11 @@ server { ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. client_max_body_size 20M; - ssl_certificate /etc/letsencrypt/live/jellyfin.loadingm.xyz/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/jellyfin.loadingm.xyz/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - ssl_trusted_certificate /etc/letsencrypt/live/jellyfin.loadingm.xyz/chain.pem; + ssl_certificate /etc/letsencrypt/live/loadingm.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/loadingm.xyz/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + ssl_trusted_certificate /etc/letsencrypt/live/loadingm.xyz/chain.pem; # Security / XSS Mitigation Headers add_header X-Content-Type-Options "nosniff"; @@ -36,6 +42,10 @@ server { # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + location / { # Proxy main Jellyfin traffic proxy_pass http://jellyfin:8096; diff --git a/nginx/sites-enabled/jellyseerr b/nginx/sites-enabled/jellyseerr new file mode 100644 index 0000000..e6ede31 --- /dev/null +++ b/nginx/sites-enabled/jellyseerr @@ -0,0 +1,62 @@ +server { + listen 80; + listen [::]:80; + server_name jellyseerr.loadingm.xyz; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Uncomment to redirect HTTP to HTTPS + location / { + return 301 https://$host$request_uri; + } +} + +server { + # Nginx versions 1.25+ + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name jellyseerr.loadingm.xyz; + + ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. + client_max_body_size 20M; + + ssl_certificate /etc/letsencrypt/live/loadingm.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/loadingm.xyz/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + ssl_trusted_certificate /etc/letsencrypt/live/loadingm.xyz/chain.pem; + + # Security / XSS Mitigation Headers + add_header X-Content-Type-Options "nosniff"; + + # Permissions policy. May cause issues with some clients + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Content Security Policy + # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + # Enforces https content and restricts JS/CSS to origin + # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. + add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + + location / { + # Proxy main jellyseerr traffic + proxy_pass http://jellyseerr:5055; + proxy_set_header Host $host; + 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 X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + + # Disable buffering when the nginx proxy gets very resource heavy upon streaming + proxy_buffering off; + } +} diff --git a/nginx/sites-enabled/karakeep b/nginx/sites-enabled/karakeep index 7ea96cd..ecb2429 100644 --- a/nginx/sites-enabled/karakeep +++ b/nginx/sites-enabled/karakeep @@ -3,8 +3,14 @@ server { listen [::]:80; server_name karakeep.loadingm.xyz; + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + # Uncomment to redirect HTTP to HTTPS - return 301 https://$host$request_uri; + location / { + return 301 https://$host$request_uri; + } } server { @@ -18,11 +24,11 @@ server { ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. client_max_body_size 20M; - ssl_certificate /etc/letsencrypt/live/karakeep.loadingm.xyz/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/karakeep.loadingm.xyz/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - ssl_trusted_certificate /etc/letsencrypt/live/karakeep.loadingm.xyz/chain.pem; + ssl_certificate /etc/letsencrypt/live/loadingm.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/loadingm.xyz/privkey.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + ssl_trusted_certificate /etc/letsencrypt/live/loadingm.xyz/chain.pem; # Security / XSS Mitigation Headers add_header X-Content-Type-Options "nosniff"; @@ -36,6 +42,10 @@ server { # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + location / { # Proxy main karakeep traffic proxy_pass http://karakeep-web:3000; diff --git a/nginx/sites-enabled/ollama b/nginx/sites-enabled/ollama index 1d74794..be96c91 100644 --- a/nginx/sites-enabled/ollama +++ b/nginx/sites-enabled/ollama @@ -1,12 +1,3 @@ -server { - listen 80; - listen [::]:80; - server_name ollama.loadingm.xyz; - - # Uncomment to redirect HTTP to HTTPS - return 301 https://$host$request_uri; -} - server { # Nginx versions 1.25+ listen 443 ssl; @@ -15,38 +6,13 @@ server { server_name ollama.loadingm.xyz; - ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. - client_max_body_size 20M; - - ssl_certificate /etc/letsencrypt/live/ollama.loadingm.xyz/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/ollama.loadingm.xyz/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - ssl_trusted_certificate /etc/letsencrypt/live/ollama.loadingm.xyz/chain.pem; - - # Security / XSS Mitigation Headers - add_header X-Content-Type-Options "nosniff"; - - # Permissions policy. May cause issues with some clients - add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always; - - # Content Security Policy - # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP - # Enforces https content and restricts JS/CSS to origin - # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. - add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + include /etc/nginx/snippets/letsencrypt.conf; + # include /etc/nginx/snippets/authelia-location.conf; location / { + include /etc/nginx/snippets/proxy.conf; + # include /etc/nginx/snippets/authelia-authrequest.conf; # Proxy main ollama traffic proxy_pass http://ollama-webui:8080; - proxy_set_header Host $host; - 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 X-Forwarded-Protocol $scheme; - proxy_set_header X-Forwarded-Host $http_host; - - # Disable buffering when the nginx proxy gets very resource heavy upon streaming - proxy_buffering off; } } diff --git a/nginx/sites-enabled/primary b/nginx/sites-enabled/primary index 9a323e6..cd845d0 100644 --- a/nginx/sites-enabled/primary +++ b/nginx/sites-enabled/primary @@ -16,6 +16,21 @@ # Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. ## +server { + listen 80 default_server; + listen [::]:80; + server_name loadingm.xyz *.loadingm.xyz; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Uncomment to redirect HTTP to HTTPS + location / { + return 301 https://$host$request_uri; + } +} + # Default server configuration # server { @@ -29,8 +44,8 @@ server { ssl_certificate /etc/letsencrypt/live/loadingm.xyz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/loadingm.xyz/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + # include /etc/letsencrypt/options-ssl-nginx.conf; + # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_trusted_certificate /etc/letsencrypt/live/loadingm.xyz/chain.pem; root /data/site; diff --git a/nginx/sites-enabled/servarr b/nginx/sites-enabled/servarr new file mode 100644 index 0000000..0887dd4 --- /dev/null +++ b/nginx/sites-enabled/servarr @@ -0,0 +1,76 @@ + +server { + # Nginx versions 1.25+ + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name servarr.loadingm.xyz; + + include /etc/nginx/snippets/letsencrypt.conf; + include /etc/nginx/snippets/authelia-location.conf; + + location /qbt/ { + # include /etc/nginx/snippets/proxy.conf; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass http://qbittorrent:8080/; + proxy_http_version 1.1; + proxy_set_header Host $proxy_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cookie_path / "/; Secure"; + } + + location ^~ /sonarr { + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass http://sonarr:8989/sonarr; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + } + # Allow the API External Access via NGINX + location ^~ /sonarr/api { + auth_basic off; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass http://sonarr:8989/sonarr/api; + } + + location ^~ /radarr { + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass http://radarr:7878/radarr; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + } + # Allow the API External Access via NGINX + location ^~ /radarr/api { + auth_basic off; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass http://radarr:7878/radarr/api; + } + + location ^~ /jackett/ { + include /etc/nginx/snippets/authelia-authrequest.conf; + add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + proxy_pass http://jackett:9117; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + } +} diff --git a/nginx/snippets/authelia-authrequest.conf b/nginx/snippets/authelia-authrequest.conf new file mode 100644 index 0000000..2bd4561 --- /dev/null +++ b/nginx/snippets/authelia-authrequest.conf @@ -0,0 +1,32 @@ +## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. +auth_request /internal/authelia/authz; + +## Save the upstream metadata response headers from Authelia to variables. +auth_request_set $user $upstream_http_remote_user; +auth_request_set $groups $upstream_http_remote_groups; +auth_request_set $name $upstream_http_remote_name; +auth_request_set $email $upstream_http_remote_email; + +## Inject the metadata response headers from the variables into the request made to the backend. +proxy_set_header Remote-User $user; +proxy_set_header Remote-Groups $groups; +proxy_set_header Remote-Email $email; +proxy_set_header Remote-Name $name; + +## Configure the redirection when the authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method' +## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url +## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily. + +## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint. +auth_request_set $redirection_url $upstream_http_location; + +## Modern Method: When there is a 401 response code from the authz endpoint redirect to the $redirection_url. +error_page 401 =302 $redirection_url; + +## Legacy Method: Set $target_url to the original requested URL. +## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module. +# set_escape_uri $target_url $scheme://$http_host$request_uri; + +## Legacy Method: When there is a 401 response code from the authz endpoint redirect to the portal with the 'rd' +## URL parameter set to $target_url. This requires users update 'auth.example.com/' with their external authelia URL. +# error_page 401 =302 https://auth.example.com/?rd=$target_url; \ No newline at end of file diff --git a/nginx/snippets/authelia-location.conf b/nginx/snippets/authelia-location.conf new file mode 100644 index 0000000..9bc3e50 --- /dev/null +++ b/nginx/snippets/authelia-location.conf @@ -0,0 +1,32 @@ +set $upstream_authelia http://authelia:9091/api/authz/auth-request; + +## Virtual endpoint created by nginx to forward auth requests. +location /internal/authelia/authz { + ## Essential Proxy Configuration + internal; + proxy_pass $upstream_authelia; + + ## Headers + ## The headers starting with X-* are required. + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Content-Length ""; + proxy_set_header Connection ""; + + ## Basic Proxy Configuration + proxy_pass_request_body off; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead + proxy_redirect http:// $scheme://; + proxy_http_version 1.1; + proxy_cache_bypass $cookie_session; + proxy_no_cache $cookie_session; + proxy_buffers 4 32k; + client_body_buffer_size 128k; + + ## Advanced Proxy Configuration + send_timeout 5m; + proxy_read_timeout 240; + proxy_send_timeout 240; + proxy_connect_timeout 240; +} diff --git a/nginx/snippets/letsencrypt.conf b/nginx/snippets/letsencrypt.conf new file mode 100644 index 0000000..378fda5 --- /dev/null +++ b/nginx/snippets/letsencrypt.conf @@ -0,0 +1,25 @@ +# Content Security Policy +# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP +# Enforces https content and restricts JS/CSS to origin +# External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. +# add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + +## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. +client_max_body_size 20M; + +ssl_certificate /etc/letsencrypt/live/loadingm.xyz/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/loadingm.xyz/privkey.pem; +# include /etc/letsencrypt/options-ssl-nginx.conf; +# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +ssl_trusted_certificate /etc/letsencrypt/live/loadingm.xyz/chain.pem; + +# Security / XSS Mitigation Headers +add_header X-Content-Type-Options "nosniff"; + +# Permissions policy. May cause issues with some clients +add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always; + +location /.well-known/acme-challenge/ { + root /var/www/certbot; +} + diff --git a/nginx/snippets/proxy.conf b/nginx/snippets/proxy.conf new file mode 100644 index 0000000..ab7b933 --- /dev/null +++ b/nginx/snippets/proxy.conf @@ -0,0 +1,34 @@ +## Headers +proxy_set_header Host $host; +proxy_set_header X-Original-URL $scheme://$http_host$request_uri; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $http_host; +proxy_set_header X-Forwarded-URI $request_uri; +proxy_set_header X-Forwarded-Ssl on; +proxy_set_header X-Forwarded-For $remote_addr; +proxy_set_header X-Real-IP $remote_addr; + +## Basic Proxy Configuration +client_body_buffer_size 128k; +proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead. +proxy_redirect http:// $scheme://; +proxy_http_version 1.1; +proxy_cache_bypass $cookie_session; +proxy_no_cache $cookie_session; +proxy_buffers 64 256k; + +## Trusted Proxies Configuration +## Please read the following documentation before configuring this: +## https://www.authelia.com/integration/proxies/nginx/#trusted-proxies +# set_real_ip_from 10.0.0.0/8; +# set_real_ip_from 172.16.0.0/12; +# set_real_ip_from 192.168.0.0/16; +# set_real_ip_from fc00::/7; +real_ip_header X-Forwarded-For; +real_ip_recursive on; + +## Advanced Proxy Configuration +send_timeout 5m; +proxy_read_timeout 360; +proxy_send_timeout 360; +proxy_connect_timeout 360; diff --git a/nginx/snippets/websockets.conf b/nginx/snippets/websockets.conf new file mode 100644 index 0000000..13d0405 --- /dev/null +++ b/nginx/snippets/websockets.conf @@ -0,0 +1,3 @@ +## WebSocket Example +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade";