Aller au contenu

Configurations de reverse proxy

Evey sert l'API et l'interface web ensemble sur le port 9091, en HTTP simple ; un reverse proxy n'a qu'à terminer le TLS et tout renvoyer vers evey:9091. Choisissez votre proxy ci-dessous (Caddy fourni, Nginx, Traefik, Nginx Proxy Manager, ou celui que vous avez déjà), puis lisez les deux sections de la fin si vous ajustez le streaming ou définissez vos propres en-têtes de sécurité.

Indiquer le proxy à Evey

Quel que soit le proxy, Evey a besoin de son adresse publique et du proxy auquel faire confiance. L'endroit pour les deux est Administration → Accès distant : une URL externe en https:// et, quand le proxy est un conteneur, son réseau dans Proxys de confiance. Les deux s'appliquent sans redémarrage. C'est une adresse en https:// qui indique à Evey qu'un proxy assure le TLS devant lui : les cookies reçoivent l'attribut Secure, l'en-tête HSTS est envoyé et l'en-tête X-Forwarded-Proto d'un proxy de confiance est pris en compte. L'interrupteur d'accès distant et l'adresse réelle du client sont expliqués dans Accès distant et HTTPS.

Les mêmes réponses peuvent venir de l'environnement, sous environment: dans le fichier compose, pour une installation entièrement décrite dans des fichiers (le panneau l'emporte quand les deux sont renseignés) :

docker-compose.yml
services:
  evey:
    environment:
      EVEY_SERVER_BASE_URL: https://media.example.com
      EVEY_TRUSTED_PROXIES: 172.16.0.0/12   # seulement si le proxy est un conteneur ; la boucle locale est toujours de confiance

L'ancienne variable EVEY_FORWARDED_PROTO reste acceptée pour forcer la valeur, mais elle est obsolète : le schéma de l'adresse suffit.

Chaque exemple fourni lit le domaine dans EVEY_DOMAIN, avec une valeur fictive dans le fichier que vous copiez : remplacez-la à cet endroit par votre domaine, sur la ligne EVEY_DOMAIN: du service du proxy (Caddy, Nginx) ou dans la règle Host(...) (Traefik).

Caddy (fourni)

Fichiers

docker/Caddyfile, construit en image par docker/Dockerfile.caddy ; le service caddy du docker-compose.yml du dépôt.

Mise en place

  1. Écrivez votre domaine sur la ligne EVEY_DOMAIN: du service caddy et assurez-vous que les ports 80 et 443 de la machine arrivent au conteneur Caddy.
  2. Ajoutez le service caddy du fichier compose du dépôt au vôtre et lancez docker compose up -d. Caddy obtient automatiquement un certificat Let's Encrypt pour EVEY_DOMAIN.
  3. Dans Administration → Accès distant, renseignez l'URL externe en https:// et 172.16.0.0/12 dans Proxys de confiance.

Ce qu'il gère déjà

Il renvoie tout vers evey:9091, les chemins de streaming étant servis sans tampon ni délai. Il ajoute les en-têtes de sécurité et la CSP de référence de la section sécurité et retire la CSP de l'application pour qu'une seule arrive au navigateur ; les deux pages qui portent leur propre politique la conservent.

Nginx

Fichiers

examples/nginx/evey.conf.template et un compose complémentaire prêt à l'emploi, examples/nginx/docker-compose.nginx.yml.

Mise en place

  1. Écrivez votre domaine sur la ligne EVEY_DOMAIN: du compose complémentaire. L'image nginx génère le modèle au démarrage en y substituant EVEY_DOMAIN.
  2. Placez le certificat et la clé là où le modèle les attend, sous les noms de fichier correspondants (/etc/ssl/certs/<domaine>.pem, /etc/ssl/private/<domaine>.key dans le volume nginx-certs) : le compose complémentaire n'obtient pas de certificat à votre place.
  3. docker compose -f docker-compose.yml -f examples/nginx/docker-compose.nginx.yml up -d.
  4. Dans Administration → Accès distant, renseignez l'URL externe en https:// et 172.16.0.0/12 dans Proxys de confiance.

Ce qu'il gère déjà

Il renvoie vers evey:9091 sans tampon ni délai sur les chemins de streaming, avec le WebSocket de SyncPlay pris en charge, X-Forwarded-For et X-Forwarded-Proto transmis, et client_max_body_size 2G pour les envois de plugins et les restaurations de sauvegarde. Il laisse passer les en-têtes de sécurité et la CSP de l'application.

Traefik

Fichiers

examples/traefik/docker-compose.traefik.yml.

Mise en place

  1. Faites tourner Traefik avec un point d'entrée websecure, un résolveur de certificats letsencrypt et un réseau Docker externe nommé proxy ; les étiquettes du compose complémentaire supposent ces noms.
  2. Écrivez votre domaine dans la règle Host(...) du compose complémentaire, puis :

    docker compose -f docker-compose.yml -f examples/traefik/docker-compose.traefik.yml up -d
    
  3. Dans Administration → Accès distant, renseignez l'URL externe en https:// et la plage du réseau proxy dans Proxys de confiance (172.16.0.0/12 couvre les réseaux Docker par défaut).

Ce qu'il gère déjà

Le compose complémentaire ajoute seulement les étiquettes du routeur et rejoint le réseau proxy. Traefik sur ses réglages par défaut ne met pas les réponses en tampon et ne plafonne pas leur durée, aucun réglage de streaming n'est donc nécessaire ; il transmet de lui-même X-Forwarded-For et X-Forwarded-Proto et laisse passer les en-têtes de sécurité de l'application.

Nginx Proxy Manager

Fichiers

Aucun : tout se règle dans l'interface de NPM. Le conteneur evey doit être sur le réseau qu'utilise NPM, voir Un proxy que vous avez déjà.

Mise en place

Dans Edit Proxy Host :

  1. Details : Scheme http, Forward Hostname evey-server, Forward Port 9091. Websockets Support : on (SyncPlay). Cache Assets : off : il mettrait en cache les segments et les playlists avec une expiration fixe. Block Common Exploits peut rester actif (désactivez-le si une recherche ou l'envoi d'un plugin répond un jour 403 sans raison).
  2. SSL : votre certificat, Force SSL, HTTP/2. HSTS est facultatif ici : l'application l'envoie déjà.
  3. Dans Administration → Accès distant, renseignez l'URL externe en https:// et la plage du réseau de NPM dans Proxys de confiance.

Ce qu'il gère déjà

NPM transmet de lui-même X-Forwarded-For, X-Forwarded-Proto et l'en-tête Host, et ses délais par défaut ne se déclenchent jamais sur les flux d'Evey. Rien d'autre n'est requis.

Facultatif : empêcher NPM de mettre en tampon les fichiers en lecture directe sur son propre disque. Advanced → Custom Nginx Configuration (l'onglet Custom Locations ne sait pas exprimer l'expression régulière) :

# Envois de plugins (jusqu'à 100 Mo) et restaurations de sauvegarde (jusqu'à 2 Go).
client_max_body_size 2G;

# Streaming : pas de tampon, pas de délai.
location ~ ^/api/v1/media/.+/(hls|stream) {
    proxy_pass http://evey-server:9091;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_read_timeout 0;
    proxy_send_timeout 0;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    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;
}

# Les scans de bibliothèque diffusent leur progression tant qu'ils tournent.
location ~ ^/api/v1/libraries/.+/scan {
    proxy_pass http://evey-server:9091;
    proxy_buffering off;
    proxy_read_timeout 0;
    proxy_send_timeout 0;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    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;
}

Tous les autres chemins (l'interface web, le reste de l'API, le WebSocket de SyncPlay) passent par le bloc de proxy de NPM, que configure l'onglet Details. Sans cet extrait, tout fonctionne : il change seulement l'endroit où les gros fichiers sont mis en tampon.

Un proxy que vous avez déjà (TLS terminé en amont)

Sur un NAS qui a déjà un reverse proxy (celui du fabricant, Nginx Proxy Manager, un Traefik sur un réseau Docker partagé), Caddy est inutile : Evey émet ses propres en-têtes de sécurité et une CSP complète, et les règles de streaming ne peuvent de toute façon être appliquées que par le proxy qui fait face au client. Un second proxy derrière lui n'ajoute rien.

Mise en place

Placez le service evey sur le réseau de ce proxy et indiquez-lui la cible evey-server:9091 (le nom du conteneur ; le nom du service, evey, fonctionne aussi) :

docker-compose.yml
services:
  evey:
    # pas de ports : le proxy est la seule porte d'entrée
    environment:
      EVEY_SERVER_BASE_URL: https://media.example.com
      EVEY_TRUSTED_PROXIES: 172.16.0.0/12     # à restreindre au sous-réseau du proxy une fois connu
    # ...
networks:
  default:
    name: proxy-net
    external: true

Ensuite, dans ce proxy, pour la route vers evey-server:9091 : transmettez X-Forwarded-For et X-Forwarded-Proto, activez la prise en charge des WebSockets, et appliquez les règles de streaming ci-dessous sur les chemins de streaming. Les sections Nginx et Nginx Proxy Manager ci-dessus donnent les lignes exactes.

Ce qu'il gère déjà

Cela dépend du proxy : vérifiez qu'un seul en-tête CSP arrive au navigateur (sections suivantes) et lancez evey admin doctor une fois les proxys de confiance renseignés.

Le streaming et les réglages par défaut d'un proxy

Evey fonctionne derrière un reverse proxy laissé sur ses réglages par défaut : chaque réponse longue envoie des octets en continu (segments HLS, lecture directe, sortie du transcodage) ou garde la connexion vivante (les flux d'événements envoient un signal de maintien), si bien qu'un délai de lecture par défaut ne se déclenche jamais. Deux points restent utiles à connaître :

  • La mise en tampon. Un proxy qui met les réponses en tampon (nginx le fait par défaut) lit un fichier en lecture directe dans ses propres tampons et fichiers temporaires avant que le client ne le reçoive. Cela fonctionne, mais un film de plusieurs gigaoctets transite alors par le disque de la machine du proxy. proxy_buffering off sur les chemins de streaming l'évite.
  • Les scans. /api/v1/libraries/*/scan diffuse sa progression tant que le scan tourne ; un proxy qui plafonne la durée totale d'une réponse (rare : nginx, Caddy et Traefik ne le font pas par défaut) la couperait. Le scan continue de toute façon côté serveur.

Les chemins concernés :

/api/v1/media/*/hls/*
/api/v1/media/*/stream
/api/v1/libraries/*/scan

Le Caddy fourni et l'exemple Nginx règlent les deux (pas de tampon, pas de délai) sur ces chemins. L'exemple Traefik et un proxy piloté par une interface comme Nginx Proxy Manager gardent leurs réglages par défaut, qui conviennent ; l'extrait NPM ci-dessus est facultatif.

En-têtes de sécurité et CSP : à lire si votre proxy définit les siens

Evey émet une Content-Security-Policy complète. Si votre proxy la laisse passer, il n'y a rien d'autre à faire.

Beaucoup d'installations la retirent au contraire (header_down -Content-Security-Policy dans Caddy) et définissent la leur. C'est possible, mais votre politique doit alors garder ces éléments, sans quoi des fonctions cessent de fonctionner sans le moindre message :

Élément Directive Ce qui ne fonctionne plus sans lui
'self' frame-src L'interface de chaque plugin et le script de chaque thème (iframe vide). Ils tournent dans /plugin-frame.html, une page d'Evey intégrée par l'application (voir plus bas).
'wasm-unsafe-eval' script-src La lecture AC-3/E-AC-3 dans le navigateur (le décodeur WebAssembly ac3go ne peut pas être compilé, le film est lu sans le son). Cet élément n'autorise que la compilation Wasm, pas eval() en général.
https://www.gstatic.com script-src (+ frame-src) Le SDK d'envoi Google Cast ne se charge jamais, et le bouton de diffusion n'apparaît plus pour personne. C'est le seul moyen pour un navigateur de découvrir les Chromecast du réseau local de l'utilisateur.
blob: media-src La lecture MSE (hls.js rattache le flux par une URL blob).
blob: worker-src hls.js se rabat silencieusement sur un démultiplexage dans le thread principal (plus saccadé sur mobile).

Une politique éprouvée pour un proxy qui définit la sienne, celle qu'envoie le Caddy fourni :

default-src 'self'; base-uri 'self'; object-src 'none';
img-src 'self' https: data: blob:; media-src 'self' blob:;
style-src 'self' 'unsafe-inline';
script-src 'self' 'wasm-unsafe-eval' https://www.gstatic.com;
connect-src 'self' https: wss:; worker-src 'self' blob:; font-src 'self' data:;
frame-src 'self' https://www.gstatic.com

base-uri et object-src ne retombent pas sur default-src : une politique qui les omet laisse libres l'injection de <base href> et l'intégration par <object>/<embed>. Gardez-les si vous définissez votre propre politique. script-src ne contient pas 'unsafe-inline' : un script injecté dans la page ne s'exécute pas. Ne le rajoutez pas, rien dans l'application n'en a besoin.

Deux pages portent leur propre politique, envoyée par l'application ; laissez l'en-tête de l'application sur ces deux chemins :

  • /plugin-frame.html, la page dans laquelle tournent les interfaces des plugins et les scripts des thèmes. Sa politique l'isole (même ouverte hors d'une iframe), limite son réseau aux hôtes déclarés dans le manifeste du plugin et ne laisse que l'application l'intégrer ; elle envoie donc aussi X-Frame-Options: SAMEORIGIN : ne le remplacez pas par DENY sur ce chemin, sinon toutes les interfaces de plugins restent vides.
  • /cast-receiver.html, la page que charge un Chromecast quand il lit depuis Evey. Le lecteur Cast y injecte des styles, des polices et des images venus de www.gstatic.com. Laissez passer l'en-tête de l'application sur ce chemin, ou appliquez cette politique à ce seul chemin :
default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://www.gstatic.com; img-src 'self' data: https: blob:; media-src 'self' blob: https:; worker-src 'self' blob:; font-src 'self' data: https://www.gstatic.com; connect-src 'self' https: wss:; frame-src https://www.gstatic.com; base-uri 'self'; object-src 'none'

Un seul en-tête CSP doit arriver au navigateur

Avec deux (celui de l'application et celui du proxy, ou celui de Caddy et l'extrait « Advanced » de Nginx Proxy Manager), le navigateur applique leur intersection : la plus stricte l'emporte, et l'échec est silencieux. Vérifiez avec curl -sI https://votre.domaine | grep -ci content-security-policy, qui doit afficher 1.

Deux vérifications de plus après tout changement :

  • Gardez la politique sur une seule ligne. Une CSP coupée sur plusieurs lignes dans la configuration d'un proxy est une corruption silencieuse classique : la jonction des lignes supprime les caractères de part et d'autre (...gstatic.com; connect-src est ainsi devenu ...gstaticonnect-src, ce qui a désactivé le SDK Cast et fusionné deux directives).
  • La page des plugins garde sa propre politique : curl -sI https://votre.domaine/plugin-frame.html | grep -i -e content-security -e x-frame doit afficher sandbox allow-scripts et SAMEORIGIN.

Adresse réelle des clients et contrôle de l'accès distant

Dès que le proxy transmet l'adresse réelle du client, Evey sait distinguer un spectateur sur le réseau domestique d'un spectateur sur Internet : c'est sur cette distinction que s'appuient le Débit distant maximal et l'interrupteur d'accès distant. Deux règles :

  • Les proxys de confiance (dans le panneau ou EVEY_TRUSTED_PROXIES) doivent couvrir le proxy : 172.16.0.0/12 quand c'est un conteneur sur le réseau Docker, rien à ajouter quand il tourne sur la machine hôte, la boucle locale étant toujours de confiance et ce que vous indiquez s'y ajoutant.
  • Des proxys en chaîne doivent chacun conserver X-Forwarded-For. Dans Caddy, c'est servers { trusted_proxies static private_ranges } ; nginx utilise proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;.

L'activer change qui est considéré comme distant : un serveur dont l'accès distant était désactivé se met à refuser les vrais clients distants (403 {"code":"remote_access_disabled"}) sans qu'aucun réglage de l'application ait changé. Evey ouvre l'accès distant de lui-même quand une adresse publique est renseignée et qu'aucun choix n'a été fait, journalise son état à chaque démarrage et chaque refus avec l'adresse du client, et evey admin doctor échoue sur « publié mais refuse tout ». Le mécanisme complet est décrit dans Accès distant et HTTPS.