Discours sur le proxy inverse et HTTPS

J’essaie de configurer Discourse derrière mon proxy inverse Apache, mais je n’arrive pas à le faire fonctionner correctement avec HTTPS.

J’ai rencontré de nombreux problèmes pour en arriver là. Pour l’instant, j’ai Discourse sur un serveur et un serveur Apache devant lui agissant comme proxy inverse. Au départ, j’ai eu beaucoup de mal à le faire fonctionner derrière un proxy inverse, car Discourse voulait toujours rediriger vers le nom d’hôte défini dans app.yaml.

D’une manière ou d’une autre, j’ai réussi à le faire fonctionner maintenant, mais je reçois des avertissements de contenu mixte dans mon navigateur.
J’ai configuré une redirection dans Apache de HTTP vers HTTPS, ce qui fonctionne bien. Mais Discourse continue de servir certains éléments en HTTP et je n’arrive pas à trouver comment le forcer à passer en HTTPS.

Par exemple, le favicon est servi en HTTP et je ne parviens pas à trouver comment modifier cela.

Puis-je faire en sorte que Discourse change tous les liens en HTTPS sans que Discourse ne gère lui-même le trafic HTTPS ?

J’ai essayé de définir :

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"

Dans Apache, mais cela ne semble pas aider.

Cocher l’option « forcer HTTPS » dans Discourse n’aide pas non plus ; cela cassera simplement le site car il ignorera tout ce qui est en HTTP.
Que dois-je faire pour éliminer le contenu mixte ?

Apache2 vous posera beaucoup de problèmes. Envisagez de passer à nginx, caddy, traefik ou haproxy.

J’ai fait fonctionner Apache2 « sans problème » dans un environnement de test, avec Apache2 agissant comme proxy inverse vers un socket Unix dans le conteneur :

La seule différence que j’ai constatée (note : seulement quelques heures de tests, rien de complet) était :

  • Apache2 ne fonctionne pas avec un lien symbolique pointant vers le socket Unix dans le volume partagé du conteneur ;
  • Apache2 était légèrement plus lent lors d’un test approximatif, mais pas de beaucoup.

Personnellement, je ne suis pas fan des guerres religieuses autour des technologies ; je ne partage donc pas l’avis selon lequel « Apache2 vous causera beaucoup de problèmes ». Je n’ai rencontré aucun problème négatif avec Apache2 lors de mes tests.

Voici la configuration de base que j’ai utilisée avec Apache2 (HTTP, qui fonctionnait parfaitement avec LETSENCRYPT, au passage) :

# cat discourse.example.conf
<VirtualHost *:80>
  ServerAdmin webmaster@localhost
  ServerName  discourse.example.com
  DocumentRoot /website/discourse

  RewriteEngine On
  ProxyPreserveHost On
  ProxyRequests Off
  ProxyPass / unix:/var/discourse/shared/socket-only/nginx.http.sock|http://localhost/
  ProxyPassReverse  / unix:/var/discourse/shared/socket-only/nginx.http.sock|http://localhost/
  ErrorLog /var/log/apache2/discourse.error.log
  LogLevel warn
  CustomLog /var/log/apache2/discourse.access.log combined

  RewriteCond %{SERVER_NAME} =discourse.example.com
  RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Note : La seule fois où nous avons rencontré des problèmes avec la diffusion de contenu HTTP même lorsque force_https était activé, c’était lorsque des fichiers manquaient dans le répertoire /uploads, mais cela (bien sûr) n’a aucun lien avec le choix entre Apache2 et nginx en tant que proxy inverse.

Merci pour vos réponses, mais je n’ai pas Apache sur le même serveur que Discourse. Je n’ai peut-être pas été assez clair à ce sujet.
J’ai un serveur Apache existant avec plusieurs sites web et je dois le configurer pour qu’il fasse office de proxy inverse pour Discourse, qui se trouve sur un autre serveur. Je ne peux donc pas utiliser de sockets.

Salutations.

Je n’ai pas essayé cette méthode, mais vous pourriez envisager de monter votre système de fichiers distant et voir si vous pouvez accéder à un socket Unix de cette manière, surtout si les serveurs se trouvent dans le même centre de données et que les performances du réseau sur une zone étendue ne posent pas de problème.

Sans fournir de détails sur l’architecture, le système d’exploitation, la configuration réseau, etc., il est difficile de répondre, et cela pourrait même être hors de portée ici sur Meta Discourse.

Soyez créatif !

La dernière fois que j’ai essayé d’utiliser cette configuration avec Apache2, j’ai rencontré des échecs de connexion au bus de messages de Discourse. Cela remonte à plus d’un an, cependant. Il semble que le site se charge correctement, mais dans la console de développement F12, il était clairement indiqué que les connexions WSS ont expiré après quelques tentatives initiales.

Vous pouvez clairement voir dans la configuration exemple que j’ai partagée que nous n’avons pas utilisé wss.

wss n’est pas équivalent à apache2 reverse proxy (ce n’est qu’une des méthodes possibles, et nous n’utilisons pas wss).

En fait, nous utilisons uniquement des configurations de proxy inverse avec nginx et apache2 via des sockets de domaine Unix car :

  • Je suis paresseux et préfère des configurations simples et faciles à déboguer.
  • Les sockets de domaine Unix sont simples et faciles à déboguer.
  • Dans nginx, nous pouvons basculer entre le proxy inverse et n’importe quel conteneur en utilisant un lien symbolique.
  • apache2 (proxy inverse vers conteneur) ne fonctionne pas avec un lien symbolique, ce qui nécessite un redémarrage du serveur web.

Cependant, @Grunskin a posé une question concernant une configuration que nous n’avons pas encore mise en place : effectuer le proxy inverse sur un hôte et exécuter le conteneur sur un autre hôte.

Quand j’aurai le temps, je testerai cela pour nginx et apache2 dans le même centre de données pour voir si je peux faire fonctionner cela en montant le système de fichiers distant et en utilisant un socket Unix.

En attendant…

Note : À mon avis, ce problème n’est pas pertinent pour nginx ou apache2 qui agissent uniquement comme des proxies inverses (mais comme mentionné, nous n’avons pas encore testé la configuration d’accès distant, je ne peux donc pas en dire plus).

Pourquoi cela est-il nécessaire ?

Discourse est une application, pas un site web. Une fois que la charge utile JavaScript initiale a été transmise à votre navigateur, de nombreuses fonctionnalités dépendent d’une connexion réactive au serveur Discourse. Le passage par un proxy via un autre système va introduire de la latence et dégrader sérieusement l’expérience utilisateur.

Pouvez-vous expliquer la raison de votre besoin ?

Il existe de nombreuses raisons d’utiliser un proxy inverse.
Par exemple, si je n’ai qu’une seule adresse IP publique et que je dois rendre plusieurs serveurs web accessibles sur les ports 80/443, et que je ne peux pas exécuter Discourse sur ce serveur web spécifique.
L’utiliser pour le déchargement SSL afin que le serveur final n’ait pas à gérer le chiffrement.
La surface d’attaque du serveur final est réduite en le plaçant derrière un proxy inverse.
Il y a beaucoup de raisons légitimes pour cela et je ne parviens pas à comprendre pourquoi cela ne serait pas possible.

Après un certain temps, je me suis souvenu que j’ai déjà un autre serveur avec Discourse exécuté derrière mon serveur Apache, et cela fonctionne bien depuis au moins deux ans. J’ai configuré le nouveau Discourse de la même manière, mais je n’arrive pas à l’empêcher de servir certains contenus en http. La seule différence entre les serveurs Discourse est que l’ancien tourne sous la version 2.4 et le nouveau sous la 2.5, donc je ne sais pas s’il y a des différences de ce côté-là ?

Comme je l’ai dit dans le premier message, force_https casse le site, rendant impossible la connexion, l’acceptation d’invitations, etc. Il semble que certains scripts JavaScript ne s’exécutent pas car ils sont probablement servis en http.
Ne serait-il pas plus logique que force_https réécrive tous les liens http en https plutôt que de les rejeter ? Du moins, offrir cela comme option.

Quelle est la méthode recommandée pour configurer Discourse en accès public ? Configurer un serveur dans une DMZ avec sa propre adresse IP externe ?

Je suggère de jeter un coup d’œil à Traefik.
Il fonctionne très bien et gère automatiquement les certificats SSL.

Il y a de nombreuses raisons d’utiliser un proxy inverse. Je suis presque certain que l’infrastructure de Discourse.org fonctionne derrière HAproxy.

J’utilise Traefik et Caddyserver, et j’ai déjà fait fonctionner nginx, HAproxy et même Apache par le passé (pour une installation WordPress dans un sous-dossier).

C’est là que réside votre problème. Vous devez activer force_https et déterminer pourquoi cela pose problème. Le désactiver n’est pas une option. Vous avez demandé une assistance gratuite et ceux qui ont répondu ne disposent pas de solution pour Apache, vous devrez donc prendre les devants concernant Apache.

Oui. Nous sommes tout à fait d’accord.

Nous utilisons désormais la configuration « deux conteneurs avec proxy inverse » sur tous nos sites, en production, pour les tests et en préproduction, à l’exception du serveur que nous utilisons pour préparer notre migration principale.

Cela s’explique par le fait qu’il est plus simple d’exécuter tous les scripts de migration lorsque PostgreSQL, MySQL et le code de l’application Discourse sont regroupés dans un seul conteneur. Ce n’est pas un environnement de production, ce qui facilite le débogage. Une fois que nous sommes satisfaits, nous déplaçons la sauvegarde vers la production et nous effectuons la restauration.

Un problème avec cette approche est que même la configuration ultra-performante à deux conteneurs avec proxy inverse ne peut compenser les temps d’arrêt lors d’une restauration de base de données, car il n’y a qu’une seule base de données.

Peut-être qu’à l’avenir, nous essaierons une configuration avec deux conteneurs de base de données afin de pouvoir restaurer l’un d’eux et basculer d’un à l’autre… du côté des données également.

Ou mieux encore, nous restaurerons dans une base de données avec un nom différent et nous basculerons en temps réel, mais nous ne savons pas encore comment procéder. Si vous savez où, dans le code, nous pouvons changer le nom de la base de données de production de discourse à discourse2 sans reconstruire l’ensemble de l’application, ce serait excellent :slight_smile: Peut-être que le super consultant créatif et innovant @pfaffman le sait ?

Mise à jour : C’est ici, dans templates/postgres.template.yml :slight_smile:

(Je vois qu’il y a effectivement des opérations mv intéressantes sur le dossier PostgreSQL ici :slight_smile: :slight_smile: ).

Les configurations autonome et multi-conteneurs ont toutes deux des avantages et des inconvénients ; mais pour la production, nous sommes entièrement passés à la configuration à deux conteneurs avec proxy inverse. Pour les tests de migration et la préproduction, la configuration autonome est la meilleure pour nous.

Concernant Apache2, j’aurais aimé avoir le temps de le configurer et de le tester avec le proxy inverse sur un serveur et les conteneurs sur un autre. Désolé pour cela…

De plus, je dois trouver un moyen de maintenir le site en ligne lorsque nous sommes en configuration à deux conteneurs et que nous effectuons une restauration de la base de données. Je vois (maintenant) que le modèle templates/postgres.template.yml est conçu pour une configuration de conteneur autonome unique.

Je veux juste ajouter que Discourse n’utilise pas WSS. Message Bus utilise le sondage long avec un codage par chunks (streaming).

Vous devez définir ce nom d’hôte sur celui par lequel Discourse sera accessible. Si le nom de domaine dans app.yml n’est pas celui que les gens tapent dans leur navigateur pour accéder à votre site, cela ne fonctionnera pas.

Vous n’avez pas les modèles ssl ou letsencrypt dans votre app.yml, n’est-ce pas ?

Vous devez effectivement activer force_https.

Et vous devez franchir quelques obstacles pour que le long polling fonctionne. Je ne me souviens plus exactement de ce qu’il faut faire.

Bonjour @Grunskin

Nous comprenons votre frustration. Cependant, lorsque vous définissez :

force_https = true

Ce n’est pas le problème. Comme @pfaffman l’a mentionné ci-dessus (au moins deux fois, l’un des principaux experts en migration de Meta) :

Vous “devez” laisser force_https = true et ensuite identifier le “véritable problème”.

Si j’étais à votre place, d’après ce que j’ai lu :

Premièrement, je configurerais votre proxy inverse sur le même serveur que le(s) conteneur(s) Discourse afin de simplifier le problème, uniquement à des fins de test et de dépannage. Assurez-vous que ce cas de test simple fonctionne parfaitement. Ensuite, une fois que cela fonctionne sur le même hôte, désactivez ce proxy inverse de test et passez à votre configuration souhaitée avec deux serveurs.

C’est un problème intéressant. Vous pouvez le résoudre si vous laissez force_https = true et que vous adoptez une méthode de dépannage structurée et étape par étape. Ce genre de problèmes sont tout simplement des énigmes informatiques qui appellent à être résolues.

Vous pouvez y arriver.


PS : Si vous vous découragez ou vous en lasser de cette énigme, vous pouvez toujours offrir une rémunération à @pfaffman ou à une autre personne expérimentée de Meta et les payer pour vous aider à franchir cet obstacle et à avancer vers des horizons plus prometteurs.


Note : Nous n’avons rencontré aucun problème pour faire fonctionner Apache2 en tant que proxy inverse avec un socket de domaine Unix sur le même serveur. Veuillez nous excuser sincèrement de ne pas avoir le temps personnel nécessaire pour configurer la configuration à deux serveurs et faire fonctionner la méthode du proxy inverse Apache2 sur deux serveurs différents. Nous sommes très occupés par les tâches finales de migration, notamment la correction des abus de “bbcode” et des problèmes de conversion vers Markdown, ce qui prend plus de temps que prévu initialement.

J’ai décidé d’installer Discourse en local sur le port 8443 avec SSL, de verrouiller le port 8443 avec UFW (pare-feu) et de faire transiter le port 443 vers le 8443 via apache2. Je teste actuellement.

                ProxyPass / "https://localhost:8443"
                ProxyPassReverse  / "https://localhost:8443"

J’ai migré notre discourse vers une nouvelle configuration et j’ai maintenant des problèmes avec http 403 sur la session POST lors de la connexion. Je suspecterais un problème CSRF, mais pour le moment, je n’ai aucune idée par où commencer le débogage et les fichiers /var/log/discourse-var-log/production.log et production_error.log ont tous 0 octet…

Des idées sur la façon de déboguer cela correctement ?

La configuration actuelle :
1. haproxy comme répartiteur de charge/accélérateur https central (pour discourse et d’autres services)

forum >> develd apache rev. proxy p.82
backend forum-backend
   mode http
   server forum.netzwissen.de 10.10.10.14:83 cookie A check
   http-request set-header X-Forwarded-Port %[dst_port]
   http-request add-header X-Forwarded-Proto https if { ssl_fc }
   # En-tête HSTS, 16000000 secondes : un peu plus de 6 mois
   http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"

2. apache local comme proxy inverse

   <IfModule proxy_module>
    ## <https://meta.discourse.org/t/running-other-websites-on-the-same-machine-as-discourse/17247>
    ProxyPreserveHost On
    # ProxyRequests Off     
    RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
    RequestHeader set X-Real-IP expr=%{REMOTE_ADDR}
    ProxyPass /  unix:/var/discourse/shared/web-only/apache.http.sock|http://localhost/
    ProxyPassReverse  / unix:/var/discourse/shared/web-only/apache.http.sock|http://localhost/
    </IfModule>

3) Discourse fonctionnant avec des conteneurs web_only et data séparés, web_only déployé avec - “templates/web.socketed.template.yml”

Voici la requête de session qui échoue lors de la connexion :

POST /session HTTP/1.1
Host: forum.netzwissen.de
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0
Accept: */*
Accept-Language: de,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 89
X-CSRF-Token: dtV0N6faVQSWZsg6z9ZGOxQBjuTpBZk6tAMRxaXJdwozF1kObw9UuiFnxbLf5OGDeL1DWDgZ5W3oJP7CY+LwRw==
Discourse-Present: true
X-Requested-With: XMLHttpRequest
Origin: https://forum.netzwissen.de
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Referer: https://forum.netzwissen.de/
Connection: keep-alive

Réponse du serveur web des conteneurs web_only :

HTTP/1.1 403 Forbidden
date: Sun, 13 Mar 2022 16:41:56 GMT
server: nginx
content-type: text/plain; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-download-options: noopen
x-permitted-cross-domain-policies: none
referrer-policy: strict-origin-when-cross-origin
vary: Accept
x-request-id: 778da942-3c1c-493b-946b-478984f53a8c
x-runtime: 0.003623
transfer-encoding: chunked
strict-transport-security: max-age=16000000; includeSubDomains; preload;

Je ne suis pas familier avec le truc csrf ni avec nginx (le serveur web externe est un apache 2.4), mais je suis à peu près sûr que le CSRF est mon problème ici car discourse fonctionne bien sans connexion et seules les requêtes POST qui sont utilisées pour la connexion échouent ici. Mon haproxy central a l’adresse IP interne 10.10.10.21, c’est pourquoi j’ai mis

set_real_ip_from 10.10.10.21/24;

dans le yml pour le discourse.conf dans le conteneur web_only. J’ai aussi essayé avec le 127.0.0.1/24; par défaut, mais les deux résultent dans la même erreur 403 lors de la connexion.