Helm: Using your own Ingress solution

The Helm chart does not require any particular Ingress solution. The Ingress resource it can create (ingress.enabled, disabled by default) is a convenience, not a requirement; any Ingress controller, Gateway API implementation, service mesh, or cloud load balancer works, as long as it fulfills the small contract below.

What Zulip needs from the proxy in front of it

Whatever routes traffic to Zulip must:

  1. Route HTTP to the chart’s Service. By default, the Service listens on port 80 and the Zulip pod serves unencrypted HTTP; see Helm: Configuring TLS.

  2. Terminate TLS, and set X-Forwarded-Proto. Zulip requires HTTPS in production. The proxy must set the X-Forwarded-Proto header to https, overriding any client-supplied value; Django uses it for CSRF checks and secure cookies.

  3. Set X-Forwarded-For, and be listed in LOADBALANCER_IPS. Zulip only trusts X-Forwarded-* headers from source addresses listed in LOADBALANCER_IPS, so that clients cannot spoof their IP addresses to evade rate limiting and audit logging. See Helm: Configuring LOADBALANCER_IPS.

  4. Pass the client’s Host: header through unchanged, and route the hostname in SETTING_EXTERNAL_HOST (and any realm subdomains of it) to the Service.

  5. Permit long-lived requests. Zulip delivers real-time events over HTTP long-polling, so idle/read timeouts on the route must be significantly longer than 60 seconds (proxy_read_timeout in nginx terms), and response buffering should be disabled if your proxy buffers by default.

These are the same requirements as for any reverse proxy in front of a standalone Zulip server; see Reverse proxies.

What stays Zulip configuration

Two settings remain with Zulip no matter which Ingress solution you choose, because they are application configuration, not proxy configuration:

  • SETTING_EXTERNAL_HOST is the canonical hostname users reach the server at. Zulip uses it to build absolute URLs in contexts where there is no request to derive a hostname from – invitation and notification emails, and API and mobile-app server URLs – and to validate the Host: header of incoming requests (Django’s ALLOWED_HOSTS), which protects against Host-header attacks such as password-reset link poisoning. It cannot safely be derived from the request.

  • LOADBALANCER_IPS tells Zulip which source addresses are your proxy layer, and hence when to believe X-Forwarded-* headers.

Both are statements of fact about the network the administrator built, which Zulip cannot discover on its own; beyond them, DNS, TLS, routing, and traffic policy are entirely up to your Ingress layer.

See also