TLS-SNI reverse proxy für yxorp

Mir geht schon länger auf die nerven dass wir die private keys für alle unsre TLS zertifikate auf dem reverse-proxy, yxorp, liegen haben müssen. Hab mich jetzt hingesetzt und das mal gelöst:

Das problem das wir da haben ist folgendes: wir haben nur eine public IPv4 IP aber viele VMs. Das einfachste und das was wir aktuell machen ist dass auf v4 port 443 halt einfach der HTTP(s) reverse proxy horch, TLS aufmacht, und dann mit http unverschlüsselt an den internen upstream server weitergeht.

Was man jetzt machen kann ist dass der reverse proxy sich den hostnamen im TLS-SNI anschaut und dann abhängig davon den upstream server auswählt und dann aber den rohen TCP stream dorthin weiterschickt, d.h. der reverse proxy muss TLS dann garnicht aufmachen!

Das ist mit dem nginx ‘stream’ modul (docs) auch eigendlich schnell gemacht, nur hast du wieder ein problem: es kann nur ein nginx modul auf port 443 horchen. Da bin ich dann bei meinem ersten versuch an dem ganzen gescheitert weil ich ja den lokalen nginx auch weiterhin verwenden will und nur nach und nach VMs auf nativ TLS umstellen will.

Inzwischen ist mir aber die idee gekommen, man könnte doch einfach die lokalen sites einfach auf ner anderen port/IP horchen lassen. Also pro http seite immer so:

server {
  listen [::1]:443 ssl;
}

und dann im nginx.conf für den TLS-SNI stuff:

stream {
  map $ssl_preread_server_name $selected_upstream {
    default local_tls
  }
  upstream local_tls {
    server [::1]:443;
  }
  server {
    listen [2001:db8::1]:443;
    proxy_pass $selected_upstream;
  }
}

Das funktioniert dann soweit aber dann hat man das nächste problem: jetzt sieht es für alle server so aus als hätten die clients alle die ::1 als IP.

Nach etwas verzweifelt nginx doku lesen findet man aber dann die nette proxy_protocol option für die http listen direktive (docs). Damit kann man die client IP weiterreichen und die seiten sehen keinen unterschied.

Dafür dann in den site configs:

server {
  listen [::1]:443 ssl proxy_protocol;
}

im globalen http {...} oder in ner conf.d/* config schalten wir das dann frei:

set_real_ip_from ::1/128;
real_ip_header proxy_protocol;

Das set_real_ip_from ist ein safety feature damit nginx nicht einfach jedem unverschlüsselt dahergelaufenem plaintext PROXY header glaubt. Hier erlauben wir das einfach nur von localhost. Das real_ip_header proxy_protocol aktiviert dann das ersetzen der nginx internen client IP mit dem was mit dem PROXY protocol daher kommt.

Zuletzt schalten wir noch in der stream modul config das senden der PROXY infos ein:

stream {
  server {
    proxy_protocol on;
  }
}

alles andere bleibt gleich.

(Das hier könnte man evtl. zu nem blogpost machen wenn man das noch etwas erweitert und noch wer proofen will)

Hab das auch so mal aufm yxorp deployed, scheint zu funktionieren. Bitte schreien wenn was kaputt is.

Blogpost TODO:

  • map stuff erklären
  • nginx auf der VM seite erklären, da braucht man zwei listen direktiven auf verschiedenen ports, eien für direct access und eine die proxy_protocol exposed
  • PROXY proto genauer anschauen?
  • Proofread. Post is ein wiki, einfach fixen wer was findet oder verbessern mag.

Deployment status:

  • web: DONEEee.
  • matrix, DONE
  • discourse, DONE
  • (git)tea, DONE

^^ALL DONE^^

Kennst du sslh - GitHub - yrutschle/sslh: Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port) ? Das nehme ich her, um meine IPv4 Adresse auf mehrere VMs mit deren $zoigs zu verteilen. Der sslh server braucht keine Zertificate, sondern horcht auf ALPN/SNI Hostnames und verteilt diese dann gemäß vorgegebenen Regeln.

z.b. /etc/sslh.cfg for host_a.acme, host_b.acme, host_c.acme und host_c_1.acme:

# Where the sslh server is listening
listen:
(
    { host: "1.2.3.4"; port: "443"; keepalive: true; },
);

# Protocols and rules that will be applied
protocols:
(
     ## https
     # match BOTH ALPN/SNI
     { name: "tls"; host: "192.168.0.1"; port: "443"; alpn_protocols: [ "http/1.1", "http/1.0", "http/2.0" ]; sni_hostnames: [ "host_a.acme" ]; log_level: 0; tfo_ok: true },
     { name: "tls"; host: "192.168.0.2"; port: "443"; alpn_protocols: [ "http/1.1", "http/1.0", "http/2.0" ]; sni_hostnames: [ "host_b.acme" ]; log_level: 0; tfo_ok: true },
     { name: "tls"; host: "192.168.0.3"; port: "443"; alpn_protocols: [ "http/1.1", "http/1.0", "http/2.0" ]; sni_hostnames: [ "host_c.acme" ]; log_level: 0; tfo_ok: true },
     { name: "tls"; host: "192.168.0.3"; port: "443"; alpn_protocols: [ "http/1.1", "http/1.0", "http/2.0" ]; sni_hostnames: [ "host_c_1.acme" ]; log_level: 0; tfo_ok: true },
);

Ja kenn ich, hab ich früher verwendet um SSH/TLS aufm gleichen port zu machen. War damals zumindest ultra segfaulty und will ich eigendlich nimma verwenden.

Wenn nginx genau das kann wiso sollt ich dann noch nen server? :slight_smile:

Soweit ich versteh hat dein setup dann aber eben genau das problem dass ich hier mit dem PROXY zeug gelöst hab, dass es für die services hinter sslh so ausschaut als würden die connections vom sslh server kommen statt von der client IP. Das is halt kake.

Bei mir tut der sslh brav und der kann auch transparent weiterleiten (was ich aber nicht verwende). Wollte auch nur darauf hinweisen als alternative zum nginx.

Solange es tut, ist ja wurscht was hergenommen wird :slight_smile:

4 posts were merged into an existing topic: Dokumentation im Discourse

das heisst also bei gelegenheit mal le direkt am web und gitea einrichten?

@robelix Ja das wär super. Ich bin noch nicht dazugekommen mich hinzuhocken und überall das Let’s Encrypt auf die VMs zu verteilen. Wenn du zeit und/oder lust hast könnten wir z.b. diesen Sonntag nen SSL Sonntag machen :slight_smile:

Ja, sollt sich ausgehen

Ah, damn. Na weisch was, den So hab ich schon was ausgemacht. Nächster ok?

Kurzes update:

In der config war noch einen bug. Das bind()en auf ne explizite IPv6 addresse in der stream modul config macht beim booten probleme, wir bekommen da nen “Cannot assign requested address” error. Warscheinlich wegen DAD oder einfach weil die router solicitaion zu lang dauert.

Der aktuelle workaround:

/etc/sysctl.d/50-its-nginx-ipv6-bind.conf:

# There's a race condition when nginx starts up, since we bind our slaac
# assigned v6 address explicitly. To work around this we just make linux shut
# up about it and let us bind to any address.
net.ipv6.ip_nonlocal_bind = 1

Sorry letzten So hab ich vergessen. Morgen?

…hätt schon versucht am web ein le-cert zu kriegen - aber derzeit frisst der nginx wohl das .well-known/acme-challenge noch weg.

Ich hab jetzt heute gerade den mail server umgestellt auf ne eigene certbot instanz. Ich glaub was ich da noch nicht dokumentiert hab is dass es einfachste is sich auf das IPv6-preferred verhalten von LE zu verlassen, d.h. du kannst den certbot einfach im standalone mode in der VM laufen lassen und solang die v6 IP der domain direkt auf die VM zeigt statt aufn yxorp funtkioniert das dann einfach (modulo firewall ;).

Ein kleines TODO für dieses setup hab ich noch: da die TLS terminierung jetzt viele male passiert muss man auch die hardening config X mal machen. Ich bereite grad ein debian config paket vor dass in /etc/nginx/conf.d ein config file für die TLS defaults dropped so dass man das einfach auf allen VMs installieren kann.

Ein leicht anderer ansatz TLS-SNI zu machen: How I'm Using SNI Proxying and IPv6 to Share Port 443 Between Webapps

Der spart sich die statische config pro domain weil er nen daemon auf IPv4:443 hat der einfach den AAAA record des gegebenen SNIs resolved und dort hin proxied. Neat.