Discursos por trás de proxy reverso e https

Estou tentando configurar o Discourse atrás do meu proxy reverso Apache, mas não consigo fazê-lo funcionar corretamente com HTTPS.

Tenho enfrentado muitos problemas para chegar até aqui. No momento, tenho o Discourse em um servidor e um servidor Apache na frente dele atuando como proxy reverso. Tive muitos problemas para fazê-lo rodar atrás de um proxy reverso inicialmente, já que o Discourse sempre queria redirecionar para o nome de host definido no app.yaml.

De alguma forma, consegui fazê-lo funcionar agora, mas estou recebendo avisos de conteúdo misto no meu navegador.
Tenho um redirecionamento no Apache de HTTP para HTTPS, então isso está funcionando bem. Mas o Discourse ainda está servindo alguns conteúdos via HTTP e não consigo descobrir como forçá-lo a mudar para HTTPS.

Por exemplo, o favicon está sendo servido via HTTP e não consigo descobrir como alterar isso.

Posso fazer com que o Discourse altere todos os links para HTTPS sem que o Discourse precise lidar com o tráfego HTTPS?

Tentei definir:

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

No Apache, mas isso não parece ajudar.

Marcar a flag force https no Discourse também não ajuda; apenas quebra o site, pois ele simplesmente ignora tudo que vem via HTTP.
O que devo fazer para eliminar o conteúdo misto?

O Apache2 pode causar muitos problemas. Considere mudar para o nginx, caddy, traefik ou haproxy.

Consegui fazer o Apache2 funcionar “sem problemas” em um ambiente de teste com o Apache2 atuando como proxy reverso para um socket Unix no container:

A única diferença que encontrei (nota: apenas algumas horas de teste, nada completo) foi:

  • O Apache2 não funciona com um link simbólico para o socket Unix no volume compartilhado no container;
  • O Apache2 foi um pouco mais lento em um teste aproximado, mas não muito.

Pessoalmente, não sou fã de guerras religiosas sobre tecnologias; portanto, discordo que “o Apache2 trará muitos problemas”. Não experimentei nenhum problema negativo com o Apache2 durante meus testes.

Aqui está a configuração principal que usei com o Apache2 (HTTP, funcionou bem com o LETSENCRYPT, aliás):

# 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: A única vez em que experimentamos problemas com o HTTP sendo servido, mesmo com force_https definido, foi quando havia arquivos faltando no diretório /uploads, mas isso (obviamente) não está relacionado ao Apache2 versus nginx como proxy reverso.

Obrigado pelas respostas, mas não tenho o Apache no mesmo servidor que o Discourse. Talvez eu não tenha sido muito claro quanto a isso.

Tenho um servidor Apache existente com vários sites e preciso que ele faça o proxy reverso do Discourse, que está localizado em outro servidor, então não posso usar sockets.

Valeu.

Não testei esse método, mas você pode considerar montar seu sistema de arquivos remoto e ver se consegue acessar um socket Unix dessa forma, especialmente se os servidores estiverem no mesmo datacenter e o desempenho da rede em uma rede de área ampla não for um problema.

Sem fornecer detalhes sobre arquitetura, sistema operacional, configuração de rede, etc., é difícil responder e talvez até esteja fora do escopo aqui no meta Discourse.

Seja criativo!

A última vez que tentei usar essa configuração com o apache2, tive falhas de conexão com o message bus do Discourse. Isso foi há mais de um ano, no entanto. Parece que o site está carregando corretamente, mas no console de desenvolvedor (F12) fica claro que as conexões WSS esgotaram o tempo após algumas tentativas iniciais.

Você pode ver claramente na configuração de exemplo que publiquei: não utilizamos wss

wss !== proxy reverso apache2 (é apenas uma das formas de fazer isso, e não utilizamos wss)

Na verdade, utilizamos apenas configurações de proxy reverso com nginx e apache2 usando sockets de domínio Unix porque:

  • Sou preguiçoso e gosto de configurações simples e fáceis de depurar.
  • sockets de domínio Unix são simples e fáceis de depurar.
  • No nginx, podemos alternar entre o proxy reverso e qualquer contêiner usando um link simbólico.
  • O apache2 (proxy reverso para contêiner) não funciona com link simbólico, exigindo reinicialização do servidor web.

No entanto, @Grunskin perguntou sobre algo que ainda não configuramos: fazer o proxy reverso em um host e executar o contêiner em outro.

Quando tiver tempo, vou testar isso tanto para nginx quanto para apache2 no mesmo data center e ver se consigo fazer funcionar montando o sistema de arquivos remoto e usando um socket Unix.

Até lá…

Nota: Na minha opinião, esse problema não é pertinente nem ao nginx nem ao apache2, que atuam apenas como proxies reversos (mas, como mencionado, ainda não testamos a configuração de acesso remoto, então não posso comentar mais sobre isso).

Por que isso é necessário?

O Discourse é um aplicativo, não um site. Uma vez que a carga inicial de JavaScript foi entregue ao seu navegador, muitos dos recursos dependem de uma conexão rápida com o servidor do Discourse. O proxy por meio de outro sistema vai introduzir latência e degradar seriamente a experiência do usuário.

Você pode explicar o motivo por trás da sua necessidade?

Existem muitas razões para usar um proxy reverso.
Por exemplo, se eu tiver apenas um IP público e precisar que vários servidores web estejam acessíveis publicamente na porta 80/443, e não puder rodar o Discourse (neste exemplo) naquele servidor web específico.
Use-o para descarregamento de SSL (SSL Offloading), para que o servidor final não precise lidar com a criptografia.
A superfície de ataque no servidor final é reduzida ao colocá-lo atrás de um proxy reverso.
Há muitas razões legítimas para isso, e não consigo entender por que isso não seria possível.

Depois de algum tempo, lembrei-me de que já tenho outro servidor com o Discourse rodando atrás do meu servidor Apache, e tem funcionado perfeitamente há pelo menos 2 anos. Configurei o novo Discourse da mesma forma, mas não consigo impedir que ele continue servindo alguns conteúdos via HTTP. A única diferença entre os servidores Discourse é que o antigo está rodando a versão 2.4 e o novo a 2.5, então não sei se há alguma diferença entre elas.

Como disse na primeira postagem, o force_https quebra o site, tornando impossível fazer login, aceitar convites etc. Parece que alguns scripts JavaScript não estão sendo executados, provavelmente porque são servidos via HTTP.
Não faria mais sentido fazer com que o force_https reescreva todos os links HTTP para HTTPS, em vez de descartá-los? Pelo menos oferecer isso como uma opção.

Qual é a maneira recomendada de configurar o Discourse publicamente? Configurar um servidor em uma DMZ com seu próprio IP externo?

Sugiro dar uma olhada no Traefik.
Ele funciona muito bem e gerencia certificados SSL automaticamente.

Havia muitas razões para usar um proxy reverso. Tenho quase certeza de que a infraestrutura do Discourse.org roda atrás do HAproxy.

Eu uso o Traefik e o Caddyserver, e já fiz o Nginx, o HAproxy e até o Apache funcionarem no passado (para uma instalação em subpasta consultando o WordPress).

Esse é o seu problema. Você precisa ativar o force_https e descobrir por que ele está quebrando. Desligá-lo não é uma opção. Você pediu suporte gratuito e quem respondeu não tem uma solução para o Apache, então você terá que ser o líder da banda do Apache.

Sim. Concordamos totalmente.

Agora usamos a configuração de “dois containers com proxy reverso” em todos os nossos sites: produção, testes e homologação, exceto no servidor que estamos usando para preparar nossa migração principal.

Isso porque é mais fácil executar todos os diversos scripts de migração quando temos o PostgreSQL, o MySQL e o código do aplicativo Discourse todos em um único container. Isso é não produtivo e mais fácil de depurar. Mas, quando estamos satisfeitos, movemos o backup para produção e restauramos.

Um problema com isso é que, mesmo a configuração super-dupla de dois containers com proxy reverso não consegue compensar o tempo de inatividade durante uma restauração de banco de dados, já que há apenas um banco de dados.

Talvez no futuro, tentemos uma configuração com dois containers de banco de dados, para que possamos restaurar um e alternar entre eles… também no lado dos dados.

Ou melhor ainda, restauraremos para um banco de dados com um nome diferente e faremos a troca em tempo real, mas ainda não sabemos como fazer isso. Se você souber onde, no código, podemos alterar o nome do banco de dados de produção de discourse para discourse2 sem recriar todo o aplicativo, isso seria ótimo :slight_smile: Talvez o criativo e inovador @pfaffman, super consultor, saiba?

Atualização: Está aqui em templates/postgres.template.yml :slight_smile:

(Vejo algumas coisas interessantes com mv na pasta do PostgreSQL aqui, com certeza :slight_smile: :).)

Tanto a configuração standalone quanto a multi-container têm vantagens e desvantagens; mas para produção, estamos totalmente comprometidos com a configuração de dois containers com proxy reverso. Para testes de migração e homologação, a configuração standalone é a melhor para nós.

Em relação ao Apache2, gostaria de ter tempo para configurar e testar isso com o proxy reverso em um servidor e os containers em outro. Desculpe por isso…

Além disso, preciso encontrar uma maneira de manter o site ativo na configuração de dois containers durante uma restauração de banco de dados. Vejo (agora) que o modelo templates/postgres.template.yml é voltado para uma configuração de container standalone único.

Apenas quero acrescentar que o Discourse não usa WSS. O Message Bus usa Long Polling com codificação em chunks (streaming).

Você precisa definir esse nome de host como o nome de host pelo qual o Discourse será acessado. Se o nome de domínio no app.yml não for o que as pessoas digitam no navegador para acessar seu site, não funcionará.

Você não tem os modelos de SSL ou Let’s Encrypt no seu app.yml, tem?

Você realmente precisa ter force_https ativado.

E você terá que contornar alguns obstáculos para fazer o polling longo funcionar. Não me lembro quais são eles.

Olá @Grunskin,

Entendemos sua frustração. No entanto, quando você define:

force_https = true

Isso não é o problema. Como @pfaffman mencionou acima (pelo menos duas vezes, sendo um dos principais especialistas em migração do Meta):

Você “deve” deixar force_https = true e, em seguida, descobrir o “problema real”.

Se eu fosse você, com base no que li:

Primeiro, configuraria seu proxy reverso no mesmo servidor que o(s) container(s) do Discourse para simplificar o problema, apenas para testes e solução de problemas. Certifique-se de que esse caso de teste simples funcione perfeitamente. Depois, quando funcionar no mesmo host, desligue esse proxy reverso de teste e avance para sua configuração desejada com dois servidores.

Esse é um problema interessante. Você consegue resolvê-lo se deixar force_https = true e mantiver um método estruturado e passo a passo de solução de problemas. Esse tipo de problema são apenas quebra-cabeças de TI implorando para serem resolvidos.

Você consegue.


PS: Se ficar desanimado ou entediado com esse quebra-cabeça, você sempre pode oferecer algum dinheiro ao @pfaffman ou a outra pessoa experiente do Meta e contratá-los para ajudá-lo a superar esse obstáculo e seguir para horizontes mais promissores.


Nota: Não tivemos nenhum problema ao fazer o Apache2 funcionar como proxy reverso com um socket de domínio Unix no mesmo servidor. Peço minhas sinceras desculpas por não ter tempo pessoal para configurar a configuração de dois servidores e fazer o método de proxy reverso do Apache2 funcionar em dois servidores diferentes. Estamos ocupados com as tarefas finais de migração, limpando “abuso de bbcode maluco” para problemas de markdown, e isso está levando mais tempo do que imaginávamos originalmente.

Decidi seguir o caminho de instalar o Discourse no localhost na porta 8443 com SSL, bloquear a porta 8443 com o UFW (firewall) e fazer o proxy da porta 443 para a 8443 usando o apache2. Testando agora.

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

Migrei nosso discourse para uma nova configuração e agora tenho problemas com http 403 em POST de sessão durante o login. Eu suspeitaria de um problema de CSRF, mas no momento não tenho ideia de onde começar a depuração e os arquivos /var/log/discourse-var-log/production.log e production_error.log estão todos com 0 bytes…

Alguma ideia de como depurar isso corretamente?

A configuração atual:
1. haproxy como balanceador de carga/acelerador https central (para discourse e outros serviços)

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: um pouco mais de 6 meses
   http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"

2. apache local como rev proxy

   <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 operando com contêineres web_only e data separados, web_only implantado com - “templates/web.socketed.template.yml”

Esta é a solicitação de sessão após o login que falha:

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

Resposta do servidor web dos contêineres 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;

Não estou familiarizado com as coisas de CSRF e também não com o nginx (o servidor web externo é um apache 2.4), mas tenho certeza de que o CSRF é o meu problema aqui, pois o discourse funciona bem sem login e apenas as requisições POST que são usadas para login falham aqui. Meu haproxy central tem o IP interno 10.10.10.21, portanto, coloquei

set_real_ip_from 10.10.10.21/24;

no yml para o discourse.conf no container web_only. Eu também tentei com o padrão 127.0.0.1/24;, mas ambos resultam no mesmo erro 403 ao fazer login.