Discussione su reverse proxy e HTTPS

Sto cercando di configurare Discourse dietro il mio proxy inverso Apache, ma non riesco a farlo funzionare correttamente con HTTPS.

Ho avuto molti problemi per arrivare fino a qui. Al momento ho Discourse su un server e un server Apache davanti che funge da proxy inverso. Inizialmente ho avuto molti problemi a farlo funzionare dietro un proxy inverso, poiché Discourse tendeva sempre a reindirizzare al nome host impostato in app.yaml.

In qualche modo ora riesco a farlo funzionare, ma ricevo avvisi di “contenuto misto” nel mio browser.
Ho configurato un reindirizzamento in Apache da HTTP a HTTPS, quindi questa parte funziona bene. Tuttavia, Discourse continua a servire alcuni contenuti tramite HTTP e non riesco a capire come forzare il passaggio a HTTPS.

Ad esempio, il favicon viene servito tramite HTTP e non riesco a capire come modificarlo.

Posso far sì che Discourse converta tutti i link in HTTPS senza che Discourse gestisca direttamente il traffico HTTPS?

Ho provato a impostare:

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

in Apache, ma sembra non funzionare.

Attivare l’opzione “force https” in Discourse non aiuta: il sito si rompe semplicemente perché ignora tutto il traffico HTTP.

Cosa devo fare per eliminare il contenuto misto?

Apache2 vi darà molti problemi. Valutate di passare a nginx, caddy, traefik o haproxy.

Ho fatto funzionare Apache2 “senza problemi” in un ambiente di test, configurandolo come reverse proxy verso una socket Unix nel contenitore:

L’unica differenza che ho riscontrato (nota: solo alcune ore di test, nulla di completo) è stata:

  • Apache2 non funziona con un collegamento simbolico alla socket Unix nel volume condiviso del contenitore;
  • Apache2 è stato leggermente più lento in un test approssimativo, ma non di molto.

Personalmente, non sono un fan delle guerre religiose sulle tecnologie; quindi non condivido l’affermazione che “Apache2 ti darà molti problemi”. Non ho riscontrato alcun problema negativo con Apache2 durante i miei test.

Ecco la configurazione di base che ho utilizzato con Apache2 (HTTP, funzionava perfettamente anche con LETSENCRYPT, a proposito):

# 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: l’unica volta in cui abbiamo riscontrato problemi con la consegna di contenuti HTTP anche quando force_https era impostato è stata quando mancavano file nella directory /uploads, ma questo (ovviamente) non è correlato a Apache2 rispetto a nginx come reverse proxy.

Grazie per le risposte, ma non ho Apache sullo stesso server di Discourse. Forse non sono stato chiaro su questo punto.
Ho un server Apache esistente con diversi siti web e ho bisogno che faccia da reverse proxy per Discourse, che si trova su un server diverso, quindi non posso utilizzare le socket.

Saluti.

Non ho provato questo metodo, ma potresti valutare di montare il tuo filesystem remoto e verificare se riesci ad accedere a un socket Unix in questo modo, specialmente se i server si trovano nello stesso datacenter e le prestazioni di rete su una rete geografica non sono un problema.

Senza fornire dettagli sull’architettura, sul sistema operativo, sulla configurazione di rete, ecc., è difficile rispondere e forse esula anche dall’ambito di meta Discourse.

Sii creativo!

L’ultima volta che ho provato a usare quella configurazione con Apache2, ho riscontrato errori di connessione al message bus di Discourse. È passato ormai più di un anno da allora. Sembra che il sito si carichi correttamente, ma nella console per sviluppatori accessibile con F12 è stato chiaramente indicato che le connessioni WSS hanno scaduto il tempo dopo alcuni tentativi iniziali.

Nella configurazione di esempio che ho pubblicato si vede chiaramente che non abbiamo utilizzato wss

wss !== proxy inverso apache2 (è solo un modo per farlo, e noi non usiamo wss)

In realtà, utilizziamo solo configurazioni di proxy inverso con nginx e apache2 tramite socket di dominio unix perché:

  • Sono pigro e preferisco configurazioni semplici e facili da debuggare.
  • I socket di dominio unix sono semplici e facili da debuggare.
  • In nginx, possiamo passare dal proxy inverso a qualsiasi container tramite un collegamento simbolico.
  • apache2 (proxy inverso verso un container) non funziona con un collegamento simbolico, quindi è necessario riavviare il server web.

Tuttavia, @Grunskin ha chiesto qualcosa che non abbiamo ancora configurato: eseguire il proxy inverso su un host ed eseguire il container su un altro host.

Quando avrò tempo, testerò questa configurazione sia per nginx che per apache2 nello stesso data center e vedrò se riesco a farla funzionare montando il file system remoto e utilizzando un socket unix.

Fino ad allora…

Nota: A mio parere, questo problema non è pertinente né per nginx né per apache2, che fungono solo da proxy inversi (ma come già menzionato, non ho ancora testato la configurazione di accesso remoto, quindi non posso commentare ulteriormente).

Perché è necessario?

Discourse è un’applicazione, non un sito web. Una volta che il carico iniziale di JavaScript è stato consegnato al tuo browser, molte funzionalità dipendono da una connessione rapida al server di Discourse. L’inoltro tramite un altro sistema introdurrà latenza e comprometterà gravemente l’esperienza utente.

Puoi spiegare il motivo alla base della tua necessità?

Ci sono molte ragioni per utilizzare un reverse proxy.
Ad esempio, se dispongo di un solo indirizzo IP pubblico e devo rendere accessibili pubblicamente più server web sulla porta 80/443, e non posso eseguire Discourse (in questo esempio) su quel particolare server web.
Utilizzarlo per l’offloading SSL, in modo che il server finale non debba gestire la crittografia.
La superficie di attacco del server finale viene ridotta posizionandolo dietro un reverse proxy.
Esistono molte ragioni legittime per farlo e non riesco a capire perché ciò non sia possibile.

Dopo un po’ mi sono ricordato di avere già un altro server con Discourse in esecuzione dietro il mio server Apache e che funziona regolarmente da almeno due anni. Ho configurato il nuovo Discourse nello stesso modo, ma non riesco a impedire che alcune risorse vengano servite tramite HTTP. L’unica differenza tra i server Discourse è che il vecchio esegue la versione 2.4 e il nuovo la 2.5, quindi non so se ci siano differenze in tal senso.

Come ho detto nel primo post, force_https rompe il sito, rendendo impossibile l’accesso, l’accettazione degli inviti, ecc. Sembra che alcuni script JavaScript non vengano eseguiti perché probabilmente vengono serviti tramite HTTP.
Non avrebbe più senso far sì che force_https riscriva tutti i link HTTP in HTTPS invece di scartarli? Almeno come opzione.

Qual è il metodo consigliato per configurare Discourse in modo pubblico? Impostare un server in una DMZ con il proprio indirizzo IP esterno?

Ti suggerisco di dare un’occhiata a Traefik.
Funziona benissimo e gestisce automaticamente i certificati SSL.

Ci sono molte ragioni per utilizzare un reverse proxy. Sono quasi certo che l’infrastruttura di Discourse.org funzioni dietro HAproxy.

Io uso Traefik e Caddyserver, e in passato ho fatto funzionare anche nginx, HAproxy e persino Apache (per un’installazione di WordPress in una sottocartella).

Questo è il tuo problema. Devi abilitare force_https e capire perché sta causando problemi. Disattivarlo non è un’opzione. Hai chiesto supporto gratuito e chi ha risposto non ha una soluzione per Apache, quindi dovrai prendere tu stesso l’iniziativa con Apache.

Sì. Siamo totalmente d’accordo.

Ora utilizziamo la configurazione “due container con reverse proxy” su tutti i nostri siti, produzione, test e staging, tranne che per il server che stiamo usando per lo staging della nostra migrazione principale.

Questo perché è più semplice eseguire tutti i vari script di migrazione quando abbiamo PostgreSQL, MySQL e il codice dell’applicazione Discourse tutti in un unico container. Questo ambiente non è di produzione ed è più facile da debuggare. Ma quando siamo soddisfatti, spostiamo il backup in produzione e ripristiniamo.

Un problema con questo approccio è che anche la configurazione super-duper a due container con reverse proxy non può compensare il downtime durante un ripristino del database, dato che c’è un solo database.

Forse in futuro proveremo una configurazione con due container del database, così da poterne ripristinare uno e alternarci tra i due anche dal lato dei dati.

O ancora meglio, potremmo ripristinare su un database con un nome diverso e fare lo switch in tempo reale, ma non sappiamo ancora come farlo. Se sapete dove nel codice possiamo cambiare il nome del database di produzione da discourse a discourse2 senza dover ricostruire l’intera applicazione, sarebbe ottimo :slight_smile: Forse il creativo e innovativo super consulente @pfaffman lo sa?

Aggiornamento: È qui in templates/postgres.template.yml :slight_smile:

(Vedo sicuramente alcune interessanti operazioni mv sulla cartella di PostgreSQL qui :slight_smile: :).)

Sia la configurazione standalone che quella multi-container hanno vantaggi e svantaggi; ma per la produzione siamo completamente orientati alla configurazione a due container con reverse proxy. Per i test di migrazione e lo staging, la configurazione standalone è la migliore per noi.

Per quanto riguarda Apache2, avrei voluto avere il tempo di configurare e testare questo setup con il reverse proxy su un server e i container su un altro. Mi scuso per questo…

Inoltre, devo trovare un modo per mantenere il sito attivo nella configurazione a due container mentre eseguo un ripristino del database. Vedo (ora) che il template templates/postgres.template.yml è orientato verso una configurazione standalone con un singolo container.

Voglio solo aggiungere che Discourse non utilizza WSS. Message Bus utilizza il Long Polling con codifica chunked (streaming).

Devi impostare quel nome host su quello con cui Discourse verrà raggiunto. Se il nome di dominio in app.yml non è quello che le persone digitano nel browser per accedere al tuo sito, non funzionerà.

Non hai i template ssl o letsencrypt nel tuo app.yml, vero?

Devi avere force_https attivato.

E devi superare alcuni ostacoli per far funzionare il long polling. Non ricordo esattamente quali siano.

Ciao @Grunskin,

Capiamo la tua frustrazione. Tuttavia, quando imposti:

force_https = true

questo non è il problema. Come ha già menzionato @pfaffman (almeno due volte, uno dei principali esperti di migrazione di Meta):

Devi “lasciare” force_https = true e poi individuare il “vero problema”.

Se fossi in te, basandomi su quanto ho letto:

Per prima cosa, configurerei il tuo reverse proxy sullo stesso server dei container di Discourse per semplificare il problema, solo a scopo di test e risoluzione dei guasti. Assicurati che questo caso di test semplice funzioni perfettamente. Poi, quando funziona sullo stesso host, disattiva quel reverse proxy di prova e passa alla configurazione desiderata con due server.

È un problema interessante. Puoi risolverlo se lasci force_https = true e mantieni un metodo strutturato di risoluzione dei problemi passo dopo passo. Questo tipo di problemi sono solo puzzle informatici che aspettano di essere risolti.

Ce la puoi fare.


PS: Se ti scoraggi o ti annoi con questo puzzle, puoi sempre offrire un compenso a @pfaffman o a un’altra persona esperta di Meta e pagare per superare questo ostacolo e procedere verso orizzonti più sereni.


Nota: Non abbiamo avuto alcun problema nel far funzionare Apache2 come reverse proxy con una socket di dominio Unix sullo stesso server. Mi scuso sinceramente per non avere il tempo personale per configurare la configurazione a due server e far funzionare il metodo reverse proxy di Apache2 su due server diversi. Siamo impegnati con le attività finali di migrazione per risolvere i problemi di “abuso folle di bbcode” verso Markdown, e sta richiedendo più tempo del previsto originariamente.

Ho deciso di seguire la strada di installare Discourse in locale sulla porta 8443 con SSL, bloccare la porta 8443 con UFW (firewall) e inoltrare la porta 443 alla 8443 tramite Apache2. Sto provando ora.

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

Ho migrato il nostro discourse a una nuova configurazione e ora ho problemi con http 403 su sessione POST durante il login. Supporrei un problema CSRF, ma al momento non ho idea da dove iniziare il debug e /var/log/discourse-var-log/production.log e production_error.log hanno tutti 0 byte…

Qualche idea su come eseguire correttamente il debug?

La configurazione attuale:
1. haproxy come load balancer/accelerator https centrale (sia per discourse che per altri servizi)

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 secondi: poco più di 6 mesi
   http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"

2. apache locale come 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 operativo con container web_only e data separati, web_only distribuito con - “templates/web.socketed.template.yml”

Questa è la richiesta di sessione al momento dell’accesso che fallisce:

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

Risposta dal webserver dei container 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;

Non ho familiarità con la roba csrf e nemmeno con nginx (il webserver esterno è un apache 2.4), ma sono abbastanza sicuro che il problema sia il CSRF, poiché discourse funziona bene senza accesso e solo le richieste POST, utilizzate per l’accesso, falliscono qui. Il mio haproxy centrale ha l’IP interno 10.10.10.21, pertanto ho inserito

set_real_ip_from 10.10.10.21/24;

nello yml per il discourse.conf nel container web_only. Ho anche provato con il valore predefinito 127.0.0.1/24;, ma entrambi risultano nello stesso errore 403 al momento dell’accesso.