Discusión detrás de proxy inverso y HTTPS

Estoy intentando configurar Discourse detrás de mi proxy inverso Apache, pero no logro que funcione correctamente con HTTPS.

He tenido muchos problemas para llegar hasta aquí. Ahora mismo tengo Discourse en un servidor y un servidor Apache delante actuando como proxy inverso. Al principio tuve muchos problemas para hacerlo funcionar detrás de un proxy inverso, ya que Discourse siempre quería redirigir al nombre de host establecido en app.yaml.

De alguna manera logré que funcionara, pero ahora recibo advertencias de contenido mixto en mi navegador.
Tengo una redirección en Apache de HTTP a HTTPS, así que eso funciona bien. Pero Discourse sigue sirviendo algunos recursos a través de HTTP y no logro averiguar cómo forzarlo a cambiarlos a HTTPS.

Por ejemplo, el favicon se sirve a través de HTTP y no sé cómo cambiarlo.

¿Puedo hacer que Discourse cambie todos los enlaces a HTTPS sin que Discourse maneje el tráfico HTTPS?

Intenté establecer:

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

en Apache, pero parece que no ayuda.

Activar la opción “force https” en Discourse tampoco ayuda; simplemente rompe el sitio, ya que ignora todo lo que llega por HTTP.
¿Qué debo hacer para eliminar el contenido mixto?

Apache2 te dará muchos problemas. Considera cambiar a nginx, caddy, traefik o haproxy.

2 Me gusta

Logré que Apache2 funcionara “sin problemas” en un entorno de prueba con Apache2 actuando como proxy inverso hacia un socket Unix en el contenedor:

La única diferencia que encontré (nota: solo unas pocas horas de pruebas, nada exhaustivo) fue:

  • Apache2 no funcionará con un enlace simbólico al socket Unix en el volumen compartido dentro del contenedor;
  • Apache2 fue un poco más lento en una prueba aproximada, pero no por mucho.

Personalmente, no soy fanático de las guerras religiosas sobre tecnologías; por lo tanto, discrepo con la afirmación de que “Apache2 te dará muchos problemas”. No experimenté ningún problema negativo con Apache2 durante mis pruebas.

Aquí está la configuración central que utilicé con Apache2 (HTTP, funcionó perfectamente con LETSENCRYPT, por cierto):

# 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>

Nota: La única vez que experimentamos problemas con la entrega de HTTP incluso cuando force_https estaba configurado fue cuando faltaban archivos en el directorio /uploads, pero esto (por supuesto) no está relacionado con Apache2 frente a nginx como proxy inverso.

1 me gusta

Gracias por las respuestas, pero no tengo Apache en el mismo servidor que Discourse. Quizás no fui muy claro al respecto.

Tengo un servidor Apache existente con varios sitios web y necesito que haga de proxy inverso para Discourse, que está ubicado en un servidor diferente, por lo que no puedo usar sockets.

Saludos.

No he probado este método, pero podrías considerar montar tu sistema de archivos remoto y ver si puedes acceder a un socket Unix de esa manera, especialmente si los servidores están en el mismo centro de datos y el rendimiento de la red a través de una red de área amplia no es un problema.

Sin proporcionar detalles sobre la arquitectura, el sistema operativo, la configuración de red, etc., es difícil responder e incluso podría estar fuera del alcance aquí en Meta Discourse.

¡Sé creativo!

1 me gusta

La última vez que intenté usar esa configuración con Apache2, tuve errores de conexión con el bus de mensajes de Discourse. Esto fue hace más de un año, aunque. Parece que el sitio se carga correctamente, pero en la consola de desarrollador F12 se indicaba claramente que las conexiones WSS se agotaron tras algunos intentos iniciales.

1 me gusta

Puedes ver claramente en la configuración de ejemplo que publiqué que no usamos wss.

wss !== proxy inverso de apache2 (es solo una forma de hacerlo, y no usamos wss)

De hecho, solo usamos configuraciones de proxy inverso con nginx y apache2 mediante sockets de dominio Unix porque:

  • Soy perezoso y me gustan las configuraciones simples y fáciles de depurar.
  • Los sockets de dominio Unix son simples y fáciles de depurar.
  • En nginx, podemos cambiar entre el proxy inverso y cualquier contenedor con un enlace simbólico.
  • apache2 (proxy inverso al contenedor) no funciona con un enlace simbólico, por lo que se requiere reiniciar el servidor web.

Sin embargo, @Grunskin preguntó sobre algo que aún no hemos configurado: realizar el proxy inverso en un host y ejecutar el contenedor en otro host.

Cuando tenga tiempo, probaré esto tanto para nginx como para apache2 en el mismo centro de datos y veré si puedo hacerlo funcionar montando el sistema de archivos remoto y usando un socket Unix.

Hasta entonces…

Nota: En mi opinión, este problema no es relevante ni para nginx ni para apache2, que solo actúan como proxies inversos (pero, como se mencionó, aún no hemos probado la configuración de acceso remoto, por lo que no puedo comentar más al respecto).

1 me gusta

¿Por qué es esto necesario?

Discourse es una aplicación, no un sitio web. Una vez que se ha entregado la carga útil inicial de JavaScript a tu navegador, muchas de las funciones dependen de una conexión rápida con el servidor de Discourse. El proxy a través de otro sistema introducirá latencia y degradará seriamente la experiencia del usuario.

¿Puedes explicar la razón detrás de tu necesidad?

2 Me gusta

Hay muchas razones para usar un proxy inverso.
Por ejemplo, si solo tengo una IP pública y necesito que varios servidores web estén accesibles en los puertos 80/443, y no puedo ejecutar Discourse en ese servidor web en particular para este ejemplo.
Úsalo como descargador de SSL para que el servidor final no tenga que gestionar el cifrado.
La superficie de ataque del servidor final se reduce al colocarlo detrás de un proxy inverso.
Hay muchas razones legítimas para esto y no logro entender por qué esto no debería ser posible.

Después de un tiempo, recordé que ya tengo otro servidor con Discourse ejecutándose detrás de mi servidor Apache y ha funcionado bien durante al menos 2 años. He configurado el nuevo Discourse de la misma manera, pero no logro que deje de servir algunos contenidos por HTTP. La única diferencia entre los servidores de Discourse es que el antiguo ejecuta la versión 2.4 y el nuevo la 2.5, así que no sé si hay alguna diferencia en ese aspecto.

Como mencioné en el primer mensaje, force_https rompe el sitio, haciendo imposible iniciar sesión, aceptar invitaciones, etc. Parece que algunos scripts de JavaScript no se ejecutan porque probablemente se sirven por HTTP.
¿No tendría más sentido que force_https reescriba todos los enlaces HTTP a HTTPS en lugar de descartarlos? Al menos que sea una opción.

¿Cuál es la forma recomendada de configurar Discourse para acceso público? ¿Configurar un servidor en una DMZ con su propia IP externa?

Sugiero que eches un vistazo a Traefik.
Funciona muy bien y gestiona los certificados SSL de forma automática.

Hubo muchas razones para ejecutar un proxy inverso. Estoy bastante seguro de que la infraestructura de Discourse.org se ejecuta detrás de HAproxy.

Yo uso Traefik y Caddyserver, y en el pasado he hecho funcionar nginx, HAproxy e incluso Apache (para una instalación en subcarpeta consultando WordPress).

Este es tu problema. Necesitas habilitar force_https y averiguar por qué está rompiendo el sitio. Desactivarlo no es una opción. Pediste soporte gratuito y quienes respondieron no tienen una solución para Apache, así que tendrás que ser el líder de la banda de Apache.

2 Me gusta

Sí. Estamos totalmente de acuerdo.

Ahora usamos la configuración de “dos contenedores con proxy inverso” en todos nuestros sitios: producción, pruebas y staging, excepto en el servidor que estamos utilizando para preparar nuestra migración principal.

Esto se debe a que es más fácil ejecutar todos los diversos scripts de migración cuando tenemos PostgreSQL, MySQL y el código de la aplicación Discourse en un solo contenedor. Esto es para entornos no productivos y facilita la depuración. Pero cuando estamos satisfechos, movemos la copia de seguridad a producción y la restauramos.

Un problema con esto es que incluso la configuración superdúper de dos contenedores con proxy inverso no puede compensar el tiempo de inactividad durante una restauración de base de datos, ya que solo hay una base de datos.

Quizás en el futuro intentemos una configuración con dos contenedores de base de datos para poder restaurar uno y alternar entre ellos… también en el lado de los datos.

O mejor aún, restauraremos a una base de datos con un nombre diferente y cambiaremos en tiempo real, pero aún no sabemos cómo hacerlo. Si sabes dónde en el código podemos cambiar el nombre de la base de datos de producción de discourse a discourse2 sin reconstruir toda la aplicación, eso sería excelente :slight_smile: ¿Quizás el creativo e innovador @pfaffman, superconsultor, lo sepa?

Actualización: ¡Está aquí en templates/postgres.template.yml :slight_smile:

(Veo que hay cosas interesantes de mv en la carpeta de postgres aquí, ¡seguro! :slight_smile: :).)

Tanto la configuración independiente como la de múltiples contenedores tienen ventajas y desventajas; pero para producción estamos totalmente enfocados en la configuración de dos contenedores con proxy inverso. Para las pruebas de migración y el entorno staging, la configuración independiente es la mejor para nosotros.

Con respecto a Apache2, desearía tener tiempo para configurar y probar esto con el proxy inverso en un servidor y los contenedores en otro. Lo siento por eso…

Además, necesito encontrar una manera de mantener el sitio en línea mientras estamos en la configuración de dos contenedores y realizamos una restauración de base de datos. Veo (ahora) que la plantilla templates/postgres.template.yml está orientada a una configuración de contenedor independiente único.

1 me gusta

Solo quiero añadir que Discourse no utiliza WSS. Message Bus usa Long Polling con codificación por fragmentos (streaming).

4 Me gusta

Debes configurar ese nombre de host al que se accederá a Discourse. Si el nombre de dominio en app.yml no es el que las personas escriben en su navegador para llegar a tu sitio, no funcionará.

¿No tienes las plantillas de ssl o letsencrypt en tu app.yml, verdad?

Sí necesitas tener force_https activado.

Además, tienes que superar algunos obstáculos para que el sondeo largo funcione. No recuerdo cuáles son.

1 me gusta

Hola @Grunskin

Entendemos tu frustración. Sin embargo, cuando configuras:

force_https = true

Esto no es el problema. Como mencionó @pfaffman anteriormente (al menos dos veces, uno de los principales expertos en migración de Meta):

Debes dejar force_https = true y luego descubrir el “problema real”.

Si yo fuera tú, basándome en lo que he leído:

Primero, configura tu proxy inverso en el mismo servidor que el contenedor (o contenedores) de Discourse para simplificar el problema, solo con fines de prueba y solución de problemas. Asegúrate de que este caso de prueba simple funcione perfectamente. Luego, cuando funcione en el mismo host, desactiva ese proxy inverso de prueba y pasa a tu configuración deseada de dos servidores.

Este es un problema interesante. Puedes resolverlo si dejas force_https = true y mantienes un método estructurado y paso a paso para la solución de problemas. Este tipo de problemas son solo acertijos informáticos que esperan ser resueltos.

Tú puedes hacerlo.


PD: Si te desalientas o te aburres con este acertijo, siempre puedes ofrecer un poco de dinero a @pfaffman o a otra persona experimentada de Meta y pagarles para que te ayuden a superar este obstáculo y avanzar hacia horizontes más claros.


Nota: No tuvimos ningún problema para que Apache2 funcionara como proxy inverso con un socket de dominio Unix en el mismo servidor. Mis sinceras disculpas por no tener tiempo personal para configurar la configuración de dos servidores y hacer que el método de proxy inverso de Apache2 funcione en dos servidores diferentes. Estamos ocupados con las tareas finales de migración, limpiando problemas de “abuso absurdo de bbcode” hacia Markdown, y esto está tomando más tiempo del que pensábamos originalmente.

1 me gusta

Decidí optar por instalar Discourse en localhost en el puerto 8443 con SSL, bloquear el puerto 8443 con UFW (firewall) y hacer proxy del puerto 443 al 8443 con Apache2. Ahora lo estoy probando.

                ProxyPass / "https://localhost:8443"
                ProxyPassReverse  / "https://localhost:8443"
1 me gusta

Migré nuestro Discourse a una nueva configuración y ahora tengo problemas con http 403 en POST session durante el inicio de sesión. Sospecho que es un problema de CSRF, pero ahora mismo no tengo ni idea de por dónde empezar a depurar y los archivos /var/log/discourse-var-log/production.log y production_error.log tienen 0 bytes…

¿Alguna idea de cómo depurar esto correctamente?

La configuración actual:
1. haproxy como balanceador de carga/acelerador https central (tanto para Discourse como para otros servicios)

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 }
   # HSTS header, 16000000 segundos: un poco más de 6 meses
   http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"

2. apache local como proxy inverso

   <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 funcionando con contenedores web_only y de datos separados, web_only desplegado con - “templates/web.socketed.template.yml”

Esta es la solicitud de sesión al iniciar sesión que falla:

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

Respuesta del servidor web de los contenedores 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;

No estoy familiarizado con el tema de CSRF ni con nginx (el servidor web externo es un Apache 2.4), pero estoy bastante seguro de que el problema es el CSRF, ya que Discourse funciona bien sin iniciar sesión y solo las solicitudes POST que se utilizan para iniciar sesión fallan aquí. Mi haproxy central tiene la IP interna 10.10.10.21, por lo tanto, he puesto

set_real_ip_from 10.10.10.21/24;

en el archivo yml para el contenedor web_only de discourse.conf. También probé con el valor predeterminado 127.0.0.1/24; pero ambos resultan en el mismo error 403 al iniciar sesión.