Gestione della "catena di fiducia" dell'IP reale dell'utente finale

Contesto

Discourse deve essere consapevole dell’indirizzo IP reale dell’utente finale.

Tuttavia, un utente finale non si connette mai direttamente a Discourse, poiché è sempre presente uno o più server web upstream (nginx in esecuzione nel container Discourse). Pertanto, abbiamo bisogno di un modo per trasmettere queste informazioni a Discourse in modo affidabile.

L’intestazione x-forwarded-for è la soluzione. In questo argomento descriverò i meccanismi specifici per gestire correttamente queste informazioni e come ci aspettiamo che vengano propagate.

Modelli

I vari modelli per la fiducia nei proxy upstream (ad esempio cloudflare.template.yml o fastly.template.yml) sono stati aggiornati per utilizzare nomi di file prevedibili negli outlet, invece di affidarsi alla sostituzione di testo (che è fragile).

Nomi dei file

server/real-ip-header.conf

Questo file contiene l’intestazione che nginx in esecuzione nel container utilizzerà come fonte di verità, ad esempio:

real_ip_header x-forwarded-for;

o come impostato nel modello Cloudflare:

real_ip_header cf-connecting-ip;

server/real-ip-recursive.conf

Se questo file esiste, controlla la ricorsione nell’elaborazione dell’intestazione “IP reale”. Dovrai abilitarlo se c’è più di un proxy davanti al container Discourse.

real_ip_recursive on;

Esempio:

Cloudflare → Load Balancer → Container Discourse (che è nginx + Discourse stesso)

Con questa configurazione, nginx riceverà un x-forwarded-for che assomiglia a:

x-forwarded-for: real_end_user_ip, cloudflare_ip

su una connessione da un IP del Load Balancer.

Per elaborarlo, nginx determina prima se l’indirizzo di origine della connessione (l’IP del Load Balancer) è affidabile (vedi set_real_ip_from) e, in tal caso, elaborerà l’ultimo IP dell’intestazione x-forwarded-for.

Poiché quell’indirizzo IP è cloudflare_ip, nginx deve quindi farlo di nuovo, controllando se cloudflare_ip è affidabile e utilizzando il successivo indirizzo IP, che è real_end_user_ip.

server/set-real-ip-from-ENVIRONMENT.conf

Questo file contiene direttive che indicano a nginx quali indirizzi IP fidare, e possiamo avere quanti file e direttive sono necessari.

I modelli in discourse_docker creano questi file quando necessario (ad esempio set-real-ip-from-cloudflare.conf) e se hai esigenze aggiuntive puoi aggiungere i tuoi.

Esempio:

Se stai eseguendo su AWS e hai un ALB davanti al container Discourse, puoi aggiungere un file aggiuntivo aggiungendo quanto segue alla tua definizione del container (adattato al tuo ambiente):

run:
  - file:
      path: /etc/nginx/conf.d/outlets/server/set-real-ip-from-aws.conf
      chmod: 644
      # AWS VPC è 10.42.0.0/16, fidati di qualsiasi connessione dalle reti ALB
      contents: |
        set_real_ip_from 10.42.66.0/24;
        set_real_ip_from 10.42.67.0/24;
  - file:
      path: /etc/nginx/conf.d/outlets/server/real-ip-header.conf
      chmod: 644
      contents: |
        real_ip_header x-forwarded-for;
5 Mi Piace

Ho un file /data/lc-manager-playbook/discourse/docker-templates/allow-local-proxy.template.yml con

after_bundle_exec:
  - replace:
    filename: /etc/nginx/conf.d/discourse.conf
    from: "types {"
    to: |
      set_real_ip_from 192.168.1.0/24;
      set_real_ip_from 192.168.11.0/24;
      set_real_ip_from 172.16.0.0/12;
      set_real_ip_from 10.0.0.0/8;
      real_ip_recursive on;
      real_ip_header X-Forwarded-For;
      types {

Sembra che funzioni ancora oggi.

C’è qualche motivo per non distribuire un modello del genere?

1 Mi Piace

Questo modello va benissimo, funzionerà oggi e nel prossimo futuro.

Fondamentalmente, si tratta di future proofing e resilienza.

Se il file dovesse cambiare, ad esempio se rimuovessimo o modificassimo la stringa types { che stai cercando, smetterebbe semplicemente di funzionare.

Se lo modifichi per usare un outlet before-server o server, continuerà a funzionare anche in quel caso.

2 Mi Piace

Qual è lo scopo di impostare X-Forwarded-For in questo momento? È previsto/consueto che contenga non solo l’IP del client (per quanto noto) ma anche la catena di proxy.

Nel messaggio del commit scrivi:

e potrebbe finire per usare `client_ip` o `proxyA_ip` a seconda del percorso di codice.

Non sarebbe questo un bug di Discourse, che non analizza/utilizza in modo coerente (risp. correttamente per qualsiasi attività in cui viene analizzato) il valore comune di quell’intestazione, piuttosto che il valore dell’intestazione (e Nginx come proxy che lo appende comunemente) sia un problema?

Se Discourse non ha bisogno di conoscere la catena di proxy, e la configurazione prevista è invece di passare solo il vero IP del client (per quanto noto), allora X-Forwarded-For perde il suo scopo. L’intestazione X-Real-IP è già impostata, corrispondente allo scopo, rendendo ora X-Forwarded-For ridondante.

È una domanda legittima: potremmo semplicemente eliminarlo e ottenere lo stesso risultato (tranne… vedi più sotto).

Il confine dell’applicazione è effettivamente nginx stesso, non Discourse o Rails. Di conseguenza, la decisione su quali proxy remoti fidarsi viene presa al punto di ingresso dell’applicazione, che è nginx. Questi può poi trasmettere tale decisione a Discourse.

Per impostazione predefinita, Rails si fida solo degli indirizzi locali quando elabora x-f-f, quindi lo gestiamo in un punto diverso dove possiamo controllarlo facilmente.

In realtà, sembra che Rails non guardi nemmeno l’intestazione x-real-ip… le intestazioni che controlla sono:

  • forwarded
  • client-ip
  • x-forwarded-for

In qualche modo è arrivato fin qui

commit 21b562852885f883be43032e03c709241e8e6d4f (tag: v0.8.0)
Author: Robin Ward
Date:   Tue Feb 5 14:16:51 2013 -0500

    Initial release of Discourse

diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf
new file mode 100644
index 00000000..62fabf4a
--- /dev/null
+++ b/config/nginx.sample.conf
...
+    proxy_set_header  X-Real-IP  $remote_addr;

Dovremo fare un po’ di indagini, ma per ora la risposta è “funziona”. Suppongo che sia così che siamo finiti in questa situazione.

Sembra che forse un gem lo utilizzi?

1 Mi Piace

Capito, quindi si tratta più di ciò che Rails o altri gem fanno con le intestazioni, e meno di ciò che il codice di Discourse fa.

È interessante notare che Rails non utilizza X-Real-IP, che è probabilmente meno comunemente usato di X-Forwarded-For, ma certamente più noto di Forwarded e Client-IP :thinking:.

Probabilmente X-Real-IP è quindi obsoleto nella configurazione di Nginx. Discourse lo espande insieme a X-Forwarded-For nei log, se lo interpreto correttamente? Non sono riuscito a trovare alcun altro uso/ menzione esplicito nel codice:

Quello qui sotto sembrava sbagliato in due modi quando l’ho visto mentre debuggavo la limitazione delle richieste condivise e ho registrato errori relativi all’IP client “unix:” non valido dopo l’aggiornamento di Discourse (usiamo un proxy socket UNIX davanti al contenitore e ci affidiamo a X-Forwarded-For).

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

Ma capisco il concetto di “funziona” per rendere affidabilmente $remote_addr l’unica fonte di verità, e real_ip_header il modo canonico per gli amministratori di controllare l’unico IP che Discourse/Rails riceve. Vedo che è già stato aggiunto a Serve Discourse from a subfolder (path prefix) instead of a subdomain :+1:.