CORS issues behind nginx ingress after updating to JupyterHub 2.1.1

Hi there, I noticed multiple issues after upgrading to JupyterHub 2.1.1 from 2.0.0.

  1. I stopped seeing spawn progress when starting the singleuser server. Logs:
[I 2022-03-04 20:57:47.436 JupyterHub log:189] 302 GET /hub/spawn -> /hub/spawn-pending/konstantin.taletskiy@labshare.org (konstantin.taletskiy@labshare.org@192.168.15.108) 1006.02ms
[I 2022-03-04 20:57:47.524 JupyterHub pages:405] konstantin.taletskiy@labshare.org is pending spawn
[I 2022-03-04 20:57:47.528 JupyterHub log:189] 200 GET /hub/spawn-pending/konstantin.taletskiy@labshare.org (konstantin.taletskiy@labshare.org@192.168.15.108) 9.75ms
[W 2022-03-04 20:57:47.746 JupyterHub base:94] Blocking Cross Origin API request.  Referer: https://<redacted_url>/hub/spawn-pending/konstantin.taletskiy@labshare.org, Host: <redacted_url>, Host URL: http://<redacted_url>/hub/
[W 2022-03-04 20:57:47.747 JupyterHub scopes:501] Not authorizing access to /hub/api/users/konstantin.taletskiy@labshare.org/server/progress. Requires any of [read:servers], not derived from scopes []
[W 2022-03-04 20:57:47.747 JupyterHub web:1787] 403 GET /hub/api/users/konstantin.taletskiy@labshare.org/server/progress (192.168.15.108): Action is not authorized with current scopes; requires any of [read:servers]
[W 2022-03-04 20:57:47.747 JupyterHub log:189] 403 GET /hub/api/users/konstantin.taletskiy@labshare.org/server/progress (@192.168.15.108) 4.93ms
  1. I can’t stop the singleuser server anymore. Logs:
[W 2022-03-04 23:07:26.200 JupyterHub base:94] Blocking Cross Origin API request.  Referer: https://<redacted_url>/hub/home, Host: <redacted_url>, Host URL: http://<redacted_url>/hub/
[W 2022-03-04 23:07:26.200 JupyterHub scopes:501] Not authorizing access to /hub/api/users/konstantin.taletskiy%40labshare.org/server. Requires any of [delete:servers], not derived from scopes []
[W 2022-03-04 23:07:26.200 JupyterHub web:1787] 403 DELETE /hub/api/users/konstantin.taletskiy%40labshare.org/server (192.168.3.68): Action is not authorized with current scopes; requires any of [delete:servers]
[W 2022-03-04 23:07:26.201 JupyterHub log:189] 403 DELETE /hub/api/users/konstantin.taletskiy%40labshare.org/server (@192.168.3.68) 8.96ms
  1. Admin page is empty

My deployment is on K8s with Helm, using heavily modified zero-to-jupyterhub-k8s chart. You could see the chart here. I am using nginx ingress, which is not described in JupyterHub reverse proxy docs, but is used in zero-to-jupyterhub-k8s, for example. I found similar issues for users running Apache and nginx reverse proxies:

The relevant parts of my config are provided below

  1. JupyterHub Docker file
FROM frolvlad/alpine-miniconda3:python3.7@sha256:bf5c88b66776e27b6745a7489104263d4ec05748eb447e7a292a0c4b5ed6c074
LABEL maintainer="Labshare <konstantin.taletskiy@labshare.org>"

# Track semantic versioning
COPY VERSION /

# Copy cull-idle script
COPY cull-idle-servers.py /srv/jupyterhub/config/cull-idle-servers.py

RUN conda install --yes -c conda-forge \
      git \
      sqlalchemy \
      tornado \
      jinja2 \
      traitlets \
      requests \
      pycurl \
      nodejs=12 \
      configurable-http-proxy \
      escapism=1.0.1 \
      jupyterhub=2.1.1 \
      python-kubernetes=22.6.0 \
      jupyterhub-kubespawner=2.0.1 && \
    pip install \
      psycopg2-binary==2.9.1  \
      oauthenticator==14.2.0

RUN mkdir -p /srv/jupyterhub/
COPY config-wrapper.py /srv/jupyterhub/config-wrapper.py
WORKDIR /srv/jupyterhub/
EXPOSE 8000

LABEL org.jupyter.service="jupyterhub"

CMD ["python", "/srv/jupyterhub/config-wrapper.py"]
  1. Nginx ingress resource template from the Helm chart:
{{- if .Values.hub.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
  name: {{ include "jupyterhub.ingress.fullname" . }}
spec:
  rules:
    - host: {{ .Values.hub.ingress.hostName }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: jupyterhub
                port:
                  number: 80
            
{{- end }}
  1. nginx configmap
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
  name: nginx-configuration
  namespace: ingress-nginx
data:
  location-snippet: |
    more_set_input_headers "X-Forwarded-Proto: https";
  proxy-body-size: "0"
  use-forwarded-headers: "true"
  use-proxy-protocol: "false"
  1. JupyterHub Portion of the nginx config generated by nginx ingress (/etc/nginx/nginx.conf file in the nginx-ingress-controller-xxxx-zzzz pod)
server {
		server_name <redacted_url> ;
		
		listen 80  ;
		listen 443  ssl http2 ;
		
		set $proxy_upstream_name "-";
		
		ssl_certificate_by_lua_block {
			certificate.call()
		}
		
		location / {
			
			set $namespace      "<jupyterhub_namespace>";
			set $ingress_name   "<helm_deployment_full_name>";
			set $service_name   "jupyterhub";
			set $service_port   "80";
			set $location_path  "/";
			
			rewrite_by_lua_block {
				lua_ingress.rewrite({
					force_ssl_redirect = false,
					ssl_redirect = true,
					force_no_ssl_redirect = false,
					use_port_in_redirects = false,
				})
				balancer.rewrite()
				plugins.run()
			}
			
			# be careful with `access_by_lua_block` and `satisfy any` directives as satisfy any
			# will always succeed when there's `access_by_lua_block` that does not have any lua code doing `ngx.exit(ngx.DECLINED)`
			# other authentication method such as basic auth or external auth useless - all requests will be allowed.
			#access_by_lua_block {
			#}
			
			header_filter_by_lua_block {
				lua_ingress.header()
				plugins.run()
			}
			
			body_filter_by_lua_block {
			}
			
			log_by_lua_block {
				balancer.log()
				
				monitor.call()
				
				plugins.run()
			}
			
			port_in_redirect off;
			
			set $balancer_ewma_score -1;
			set $proxy_upstream_name "<k8s_namespace-service_name-port>;
			set $proxy_host          $proxy_upstream_name;
			set $pass_access_scheme  $scheme;
			
			set $pass_server_port    $server_port;
			
			set $best_http_host      $http_host;
			set $pass_port           $pass_server_port;
			
			set $proxy_alternative_upstream_name "";
			
			client_max_body_size                    0;
			
			proxy_set_header Host                   $best_http_host;
			
			# Pass the extracted client certificate to the backend
			
			# Allow websocket connections
			proxy_set_header                        Upgrade           $http_upgrade;
			
			proxy_set_header                        Connection        $connection_upgrade;
			
			proxy_set_header X-Request-ID           $req_id;
			proxy_set_header X-Real-IP              $remote_addr;
			
			proxy_set_header X-Forwarded-For        $remote_addr;
			
			proxy_set_header X-Forwarded-Host       $best_http_host;
			proxy_set_header X-Forwarded-Port       $pass_port;
			proxy_set_header X-Forwarded-Proto      $pass_access_scheme;
			
			proxy_set_header X-Scheme               $pass_access_scheme;
			
			# Pass the original X-Forwarded-For
			proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
			
			# mitigate HTTPoxy Vulnerability
			# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
			proxy_set_header Proxy                  "";
			
			# Custom headers to proxied server
			
			proxy_connect_timeout                   5s;
			proxy_send_timeout                      60s;
			proxy_read_timeout                      60s;
			
			proxy_buffering                         off;
			proxy_buffer_size                       4k;
			proxy_buffers                           4 4k;
			
			proxy_max_temp_file_size                1024m;
			
			proxy_request_buffering                 on;
			proxy_http_version                      1.1;
			
			proxy_cookie_domain                     off;
			proxy_cookie_path                       off;
			
			# In case of errors try the next upstream server before returning an error
			proxy_next_upstream                     error timeout;
			proxy_next_upstream_timeout             0;
			proxy_next_upstream_tries               3;
			
			# Custom code snippet configured in the configuration configmap
			more_set_input_headers "X-Forwarded-Proto: https";
			
			proxy_pass http://upstream_balancer;
			
			proxy_redirect                          off;
			
		}
		
	}

The only change that I did from the working configuration was to change jupyterhub=2.0.0 to jupyterhub=2.1.1 in the Docker file and upgrade the Helm Chart. Could anyone suggest the possible solutions to fix the problem?

So far, I looked into adding config snippet to my JupyterHub ingress definitions to make it look more like the one provided in the documentation:

nginx.ingress.kubernetes.io/configuration-snippet: |
  proxy_set_header Host $http_host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection upgrade;

That didn’t help. Please help.

1 Like

This probably indicates that either

  • your Nginx header isn’t passed to JupyterHub (maybe the config isn’t being applied?)
  • You’ve got another proxy somewhere (e.g. a loadbalancer or something else in front of K8S?) that’s setting a header

Are you able to add some debug logging to find out what headers Nginx is sending/receiving?

There is indeed a load balancer in front of the nginx. Here’s my attempt to obtain some debug logging (as instructed here: Troubleshooting - NGINX Ingress Controller with --v=5) after trying to stop my server

2022/03/14 22:44:20 [debug] 39#39: *16329 http uri: "/hub/api/users/konstantin.taletskiy@labshare.org/server"
2022/03/14 22:44:20 [debug] 39#39: *16329 http args: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http exten: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 posix_memalign: 00005561715FA1A0:4096 @16
2022/03/14 22:44:20 [debug] 39#39: *16329 http process request header line
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Host: <redacted_url>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Connection: keep-alive"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "sec-ch-ua: "Microsoft Edge";v="95", "Chromium";v="95", ";Not A Brand";v="99""
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Accept: */*"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Content-Type: application/json"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "X-Requested-With: XMLHttpRequest"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "sec-ch-ua-mobile: ?0"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.53"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "sec-ch-ua-platform: "macOS""
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Origin: https://<redacted_url>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Sec-Fetch-Site: same-origin"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Sec-Fetch-Mode: cors"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Sec-Fetch-Dest: empty"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Referer: https://<redacted_url>/hub/home"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Accept-Encoding: gzip, deflate, br"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Accept-Language: en-US,en;q=0.9"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header: "Cookie: jupyterhub-hub-login="<redacted>"; jupyterhub-session-id=<redacted>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http header done
2022/03/14 22:44:20 [debug] 39#39: *16329 generic phase: 0
2022/03/14 22:44:20 [debug] 39#39: *16329 rewrite phase: 1
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "-"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $proxy_upstream_name
2022/03/14 22:44:20 [debug] 39#39: *16329 test location: "/"
2022/03/14 22:44:20 [debug] 39#39: *16329 using configuration "/"
2022/03/14 22:44:20 [debug] 39#39: *16329 http cl:-1 max:0
2022/03/14 22:44:20 [debug] 39#39: *16329 rewrite phase: 3
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "<jupyterhub_namespace>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $namespace
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "<helm_deployment_full_name>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $ingress_name
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "jupyterhub"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $service_name
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "80"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $service_port
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "/"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $location_path
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "-1"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $balancer_ewma_score
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: "<k8s_namespace-service_name-port>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $proxy_upstream_name
2022/03/14 22:44:20 [debug] 39#39: *16329 http script complex value
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "<k8s_namespace-service_name-port>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $proxy_host
2022/03/14 22:44:20 [debug] 39#39: *16329 http script complex value
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "http"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $pass_access_scheme
2022/03/14 22:44:20 [debug] 39#39: *16329 http script complex value
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "80"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $pass_server_port
2022/03/14 22:44:20 [debug] 39#39: *16329 http script complex value
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "<redacted_url>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $best_http_host
2022/03/14 22:44:20 [debug] 39#39: *16329 http script complex value
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "80"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $pass_port
2022/03/14 22:44:20 [debug] 39#39: *16329 http script value: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http script set $proxy_alternative_upstream_name
2022/03/14 22:44:20 [debug] 39#39: *16329 rewrite phase: 4
2022/03/14 22:44:20 [debug] 39#39: *16329 lua rewrite handler, uri:"/hub/api/users/konstantin.taletskiy@labshare.org/server" c:1
2022/03/14 22:44:20 [debug] 39#39: *16329 looking up Lua code cache with key '=rewrite_by_lua(nginx.conf:1255)nhli_810dc96928b7314c9cd60e7b526c7375'
2022/03/14 22:44:20 [debug] 39#39: *16329 lua creating new thread
2022/03/14 22:44:20 [debug] 39#39: *16329 lua reset ctx
2022/03/14 22:44:20 [debug] 39#39: *16329 http cleanup add: 00005561715FA990
2022/03/14 22:44:20 [debug] 39#39: *16329 lua run thread, top:0 c:1
2022/03/14 22:44:20 [debug] 39#39: *16329 add cleanup: 00005561715FAA08
2022/03/14 22:44:20 [debug] 39#39: *16329 lua resume returned 0
2022/03/14 22:44:20 [debug] 39#39: *16329 lua light thread ended normally
2022/03/14 22:44:20 [debug] 39#39: *16329 lua deleting light thread
2022/03/14 22:44:20 [debug] 39#39: *16329 rewrite phase: 5
2022/03/14 22:44:20 [debug] 39#39: *16329 headers more rewrite handler, uri "/hub/api/users/konstantin.taletskiy@labshare.org/server"
2022/03/14 22:44:20 [debug] 39#39: *16329 post rewrite phase: 6
2022/03/14 22:44:20 [debug] 39#39: *16329 generic phase: 7
2022/03/14 22:44:20 [debug] 39#39: *16329 generic phase: 8
2022/03/14 22:44:20 [debug] 39#39: *16329 generic phase: 9
2022/03/14 22:44:20 [debug] 39#39: *16329 access phase: 10
2022/03/14 22:44:20 [debug] 39#39: *16329 access phase: 11
2022/03/14 22:44:20 [debug] 39#39: *16329 access phase: 12
2022/03/14 22:44:20 [debug] 39#39: *16329 access phase: 13
2022/03/14 22:44:20 [debug] 39#39: *16329 post access phase: 14
2022/03/14 22:44:20 [debug] 39#39: *16329 generic phase: 15
2022/03/14 22:44:20 [debug] 39#39: *16329 generic phase: 16
2022/03/14 22:44:20 [debug] 39#39: *16329 http init upstream, client timer: 0
2022/03/14 22:44:20 [debug] 39#39: *16329 http map started
2022/03/14 22:44:20 [debug] 39#39: *16329 http map: "" ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http map started
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "<redacted>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http map: "" "<redacted>"
2022/03/14 22:44:20 [debug] 39#39: *16329 posix_memalign: 00005561715FC400:4096 @16
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "Host"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "<redacted_url>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "X-Request-ID"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "<redacted>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "X-Real-IP"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "192.168.3.68"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "X-Forwarded-For"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "192.168.3.68"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "X-Forwarded-Host"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "<redacted_url>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "X-Forwarded-Port"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "80"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "X-Forwarded-Proto"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "http"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: "X-Scheme"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script var: "http"
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http script copy: ""
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "sec-ch-ua: "Microsoft Edge";v="95", "Chromium";v="95", ";Not A Brand";v="99""
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Accept: */*"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Content-Type: application/json"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "X-Requested-With: XMLHttpRequest"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "sec-ch-ua-mobile: ?0"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.53"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "sec-ch-ua-platform: "macOS""
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Origin: https://<redacted_url>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Sec-Fetch-Site: same-origin"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Sec-Fetch-Mode: cors"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Sec-Fetch-Dest: empty"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Referer: https://<redacted_url>/hub/home"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Accept-Encoding: gzip, deflate, br"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Accept-Language: en-US,en;q=0.9"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header: "Cookie: jupyterhub-hub-login="<redacted>"; jupyterhub-session-id=<redacted>"
2022/03/14 22:44:20 [debug] 39#39: *16329 http proxy header:
"DELETE /hub/api/users/konstantin.taletskiy%40labshare.org/server HTTP/1.1
Host: <redacted_url>
X-Request-ID: <redacted>
X-Real-IP: 192.168.3.68
X-Forwarded-For: 192.168.3.68
X-Forwarded-Host: <redacted_url>
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Scheme: http
sec-ch-ua: "Microsoft Edge";v="95", "Chromium";v="95", ";Not A Brand";v="99"
Accept: */*
Content-Type: application/json
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.53
sec-ch-ua-platform: "macOS"
Origin: https://<redacted_url>
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://<redacted_url>/hub/home
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: jupyterhub-hub-login="<redacted>"; jupyterhub-session-id=<redacted>

"

Can you check the documentation for your load balancer? You might need to configure it to set some forwarding headers. At the moment I’m guessing Nginx is treating your load balancer as the source, which is making a http request to Nginx.

You’ll then need to ensure Nginx passes those headers on instead of setting them.

I am using AWS’s Classic Load Balancer

Documentation states the following:

Application Load Balancers and Classic Load Balancers automatically add X-Forwarded-For , X-Forwarded-Proto , and X-Forwarded-Port headers to the request.

So, I am not sure where the issue lies: in load balancer, nginx ingress or JupyterHub?

1 Like

If the load balancer is already setting the headers then you need to ensure Nginx passes them on. I think (though I’m not sure) if you remove your current Nginx header configuration the headers should be passed through unmodified, try checking using your debugging config, I’d expect

to mention https and the external client IP (I’m assuming 192.168.3.68 is your load-balancer IP?). You may need to override some headers if JupyterHub is using the wrong one, you can see the logic in use outermost proxied entry when looking up browser protocol by minrk · Pull Request #3757 · jupyterhub/jupyterhub · GitHub

2 Likes

I can confirm that the protocol header is lost on the way. I have changed jupyterhub/utils.py

def get_browser_protocol(request):
  ...
  # second choice: X-Scheme or X-Forwarded-Proto
 proto_header = 'https'

and the admin interface works as expected.

I can now also confirm that when I configure nginx (our reverse proxy)

proxy_set_header X-Scheme https;

instead of proxy_set_header X-Scheme $scheme; I can see the admin interface.

Thanks @fkapheris, I can verify that modifying

proto_header = 'https'

fixes the issue.

Now, for more permanent solution, I had to do a deep dive in nginx ingress. Turns out, the only working way to rewrite X-Forwarded-Proto, X-Forwarded-Scheme and X-Forwarded-Port when terminating SSL at the Load Balancer is to write a custom nginx Lua plugin, as suggested on Github. You need to create a configMap with the code of Lua plugin, mount the configMap as a volume to nginx-ingress controller and enable plugin in the ingress-nginx-controller configMap:

apiVersion: v1
data:
  ...
  plugins: rewrite_fwd_headers
kind: ConfigMap
metadata:
  ...
  name: ingress-nginx-controller
  namespace: ingress-nginx

Please, keep in mind that old versions of nginx ingress can’t load Lua plugin, even when mounted and configured. You have to update to the latest version.

Issue is now resolved for me