From 80fc37805bf7fc388dbc7009a65aeaf4aedeca08 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Sun, 20 Oct 2024 00:04:36 +0100 Subject: [PATCH 1/7] Support subdomain_host (CHP needs --host-routing) --- jupyterhub/templates/proxy/deployment.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jupyterhub/templates/proxy/deployment.yaml b/jupyterhub/templates/proxy/deployment.yaml index 85220a86ef..3cf3b345fc 100644 --- a/jupyterhub/templates/proxy/deployment.yaml +++ b/jupyterhub/templates/proxy/deployment.yaml @@ -100,6 +100,9 @@ spec: {{- if .Values.debug.enabled }} - --log-level=debug {{- end }} + {{- if .Values.hub.config.subdomain_host }} + - --host-routing + {{- end }} {{- range .Values.proxy.chp.extraCommandLineFlags }} - {{ tpl . $ }} {{- end }} From 2e926dede793bffd2d44e46cdb5e08cf644106db Mon Sep 17 00:00:00 2001 From: Simon Li Date: Mon, 21 Oct 2024 16:48:24 +0100 Subject: [PATCH 2/7] Add schema docs for subdomain_host --- jupyterhub/values.schema.yaml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/jupyterhub/values.schema.yaml b/jupyterhub/values.schema.yaml index 769038f55a..601cae68ab 100644 --- a/jupyterhub/values.schema.yaml +++ b/jupyterhub/values.schema.yaml @@ -217,7 +217,7 @@ properties: values, you need to use [`hub.extraConfig`](schema_hub.extraConfig) instead. - ```{admonition} Currently intended only for auth config + ```{admonition} Some configuration must be set in multiple places :class: warning This config _currently_ (0.11.0) only influence the software in the `hub` Pod, but some Helm chart config options such as @@ -271,6 +271,24 @@ properties: the `--values` or `-f` flag. During merging, lists are replaced while dictionaries are updated. ``` + properties: + subdomain_host: + type: string + description: | + The subdomain to use for hosting singleuser servers. + + This helps protect against some cross-origin attacks by giving each user + their own subdomain `.subdomain.example.org`. + + It requires a wildcard DNS `*.subdomain.example.org` and SSL certificate. + If you are using a Kubernetes ingress this must include hosts + `subdomain.example.org` and `*.subdomain.example.org`. + + The chart does not support the automatic creation of wildcard HTTPS certificates. + Instead you should obtain a certificate from an external source, + or install an ACME client such as cert-manager with the DNS-01 challenge. + + See {ref}`jupyterhub:subdomains` in the JupyterHub documentation. extraFiles: &extraFiles type: object additionalProperties: false From 9658c24b9e9b1ed0907819fc02436079b9592ea4 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Mon, 21 Oct 2024 17:04:17 +0100 Subject: [PATCH 3/7] Move subdomain docs from schema to the security page --- docs/source/administrator/security.md | 41 +++++++++++++++++++++++++++ jupyterhub/values.schema.yaml | 12 ++------ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/docs/source/administrator/security.md b/docs/source/administrator/security.md index e25c5b4b33..a8f0761419 100644 --- a/docs/source/administrator/security.md +++ b/docs/source/administrator/security.md @@ -489,3 +489,44 @@ proxy: ``` This would restrict the access to only two IP addresses: `111.111.111.111` and `222.222.222.222`. + +(jupyterhub_subdomains)= + +## Host user servers on a subdomain + +You can reduce the chance of cross-origin attacks by giving each user +their own subdomain `.jupyter.example.org`. +This requires setting [`hub.config.subdomain_host`](schema_hub.config.subdomain_host), creating a wildcard DNS record `*.jupyter.example.org`, and creating a wildcard SSL certificate. + +```yaml +hub: + config: + JupyterHub: + subdomain_host: jupyter.example.org +``` + +If you are using a Kubernetes ingress this must include hosts +`jupyter.example.org` and `*.jupyter.example.org`. +For example: + +```yaml +ingress: + enabled: true + hosts: + - jupyter.example.org + - "*.jupyter.example.org" + tls: + - hosts: + - jupyter.example.org + - "*.jupyter.example.org" + secretName: example-tls +``` + +where `example-tls` is the name of a Kubernetes secret containing the wildcard certificate and key. + +The chart does not support the automatic creation of wildcard HTTPS certificates. +You must obtain a certificate from an external source, +for example by using an ACME client such as [cert-manager with the DNS-01 challenge](https://cert-manager.io/docs/configuration/acme/dns01/), +and ensure the certificate and key are stored in the secret. + +See {ref}`jupyterhub:subdomains` in the JupyterHub documentation for more information. diff --git a/jupyterhub/values.schema.yaml b/jupyterhub/values.schema.yaml index 601cae68ab..cf14271ccc 100644 --- a/jupyterhub/values.schema.yaml +++ b/jupyterhub/values.schema.yaml @@ -278,17 +278,9 @@ properties: The subdomain to use for hosting singleuser servers. This helps protect against some cross-origin attacks by giving each user - their own subdomain `.subdomain.example.org`. + their own subdomain `.jupyter.example.org`. - It requires a wildcard DNS `*.subdomain.example.org` and SSL certificate. - If you are using a Kubernetes ingress this must include hosts - `subdomain.example.org` and `*.subdomain.example.org`. - - The chart does not support the automatic creation of wildcard HTTPS certificates. - Instead you should obtain a certificate from an external source, - or install an ACME client such as cert-manager with the DNS-01 challenge. - - See {ref}`jupyterhub:subdomains` in the JupyterHub documentation. + See {ref}`jupyterhub_subdomains`. extraFiles: &extraFiles type: object additionalProperties: false From 7152952846bd705795d3e521b2b9801831faca6b Mon Sep 17 00:00:00 2001 From: Simon Li Date: Mon, 21 Oct 2024 17:13:34 +0100 Subject: [PATCH 4/7] Fix location of subdomain_host in schema --- docs/source/administrator/security.md | 2 +- jupyterhub/values.schema.yaml | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/source/administrator/security.md b/docs/source/administrator/security.md index a8f0761419..d6cf58c788 100644 --- a/docs/source/administrator/security.md +++ b/docs/source/administrator/security.md @@ -496,7 +496,7 @@ This would restrict the access to only two IP addresses: `111.111.111.111` and ` You can reduce the chance of cross-origin attacks by giving each user their own subdomain `.jupyter.example.org`. -This requires setting [`hub.config.subdomain_host`](schema_hub.config.subdomain_host), creating a wildcard DNS record `*.jupyter.example.org`, and creating a wildcard SSL certificate. +This requires setting [`subdomain_host`](schema_hub.config.JupyterHub.subdomain_host), creating a wildcard DNS record `*.jupyter.example.org`, and creating a wildcard SSL certificate. ```yaml hub: diff --git a/jupyterhub/values.schema.yaml b/jupyterhub/values.schema.yaml index cf14271ccc..e02fb5b0cf 100644 --- a/jupyterhub/values.schema.yaml +++ b/jupyterhub/values.schema.yaml @@ -272,15 +272,19 @@ properties: dictionaries are updated. ``` properties: - subdomain_host: - type: string - description: | - The subdomain to use for hosting singleuser servers. + JupyterHub: + type: object + additionalProperties: true + properties: + subdomain_host: + type: string + description: | + The subdomain to use for hosting singleuser servers. - This helps protect against some cross-origin attacks by giving each user - their own subdomain `.jupyter.example.org`. + This helps protect against some cross-origin attacks by giving each user + their own subdomain `.jupyter.example.org`. - See {ref}`jupyterhub_subdomains`. + See {ref}`jupyterhub_subdomains`. extraFiles: &extraFiles type: object additionalProperties: false From 784eb56026d5e800b799629d362a862a6acf10dc Mon Sep 17 00:00:00 2001 From: Simon Li Date: Mon, 21 Oct 2024 17:23:23 +0100 Subject: [PATCH 5/7] schema: add description for hub.config.JupyterHub --- jupyterhub/values.schema.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jupyterhub/values.schema.yaml b/jupyterhub/values.schema.yaml index e02fb5b0cf..4d735ca20c 100644 --- a/jupyterhub/values.schema.yaml +++ b/jupyterhub/values.schema.yaml @@ -275,6 +275,11 @@ properties: JupyterHub: type: object additionalProperties: true + description: | + JupyterHub Traitlets configuration. + + See {py:mod}`jupyterhub:jupyterhub.app` for the full list, + but take note of the [above warnings](schema_hub.config). properties: subdomain_host: type: string From a0864f176c35775f5e4aab20819d1f1871a14f5a Mon Sep 17 00:00:00 2001 From: Simon Li Date: Mon, 21 Oct 2024 18:08:28 +0100 Subject: [PATCH 6/7] deployment.yaml: JupyterHub.subdomain_host Co-authored-by: Erik Sundell --- jupyterhub/templates/proxy/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/templates/proxy/deployment.yaml b/jupyterhub/templates/proxy/deployment.yaml index 3cf3b345fc..d5a987824d 100644 --- a/jupyterhub/templates/proxy/deployment.yaml +++ b/jupyterhub/templates/proxy/deployment.yaml @@ -100,7 +100,7 @@ spec: {{- if .Values.debug.enabled }} - --log-level=debug {{- end }} - {{- if .Values.hub.config.subdomain_host }} + {{- if .Values.hub.config.JupyterHub.subdomain_host }} - --host-routing {{- end }} {{- range .Values.proxy.chp.extraCommandLineFlags }} From dcd031a5b9edb2b99036503e5f284ad8fa0d4661 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Wed, 23 Oct 2024 23:33:43 +0100 Subject: [PATCH 7/7] Test subdomain_host --- .github/workflows/test-chart.yaml | 6 ++++- tests/test_spawn.py | 43 ++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-chart.yaml b/.github/workflows/test-chart.yaml index 6885fe7be2..907beb69c0 100644 --- a/.github/workflows/test-chart.yaml +++ b/.github/workflows/test-chart.yaml @@ -137,13 +137,14 @@ jobs: --set hub.image.name=quay.io/jupyterhub/k8s-hub-slim --set prePuller.hook.enabled=true --set prePuller.hook.pullOnlyOnChanges=true - - k3s-channel: v1.31 # also test hub.existingSecret + - k3s-channel: v1.31 # also test hub.existingSecret and subdomain_host test: install local-chart-extra-args: >- --set hub.existingSecret=test-hub-existing-secret --set proxy.secretToken=aaaa1111 --set hub.cookieSecret=bbbb2222 --set hub.config.CryptKeeper.keys[0]=cccc3333 + --set hub.config.JupyterHub.subdomain_host=jupyterhub.example.org create-k8s-test-resources: true # We run three upgrade tests where we first install an already released @@ -368,6 +369,9 @@ jobs: continue-on-error: ${{ matrix.accept-failure == true }} run: | . ./ci/common + if [ "${{ contains(matrix.local-chart-extra-args, 'subdomain_host') }}" = "true" ]; then + export CI_SUBDOMAIN_HOST=jupyterhub.example.org + fi # If you have problems with the tests add '--capture=no' to show stdout pytest --verbose --maxfail=2 --color=yes ./tests diff --git a/tests/test_spawn.py b/tests/test_spawn.py index a20a2dc2b2..86cd325da1 100644 --- a/tests/test_spawn.py +++ b/tests/test_spawn.py @@ -1,10 +1,14 @@ import json +import os import subprocess import time import pytest import requests +# If we're testing subdomain hosts in GitHub CI our workflow will set this +CI_SUBDOMAIN_HOST = os.getenv("CI_SUBDOMAIN_HOST") + def test_spawn_basic( api_request, @@ -28,12 +32,39 @@ def test_spawn_basic( api_request, jupyter_user, request_data["test_timeout"] ) assert server_model - r = requests.get( - request_data["hub_url"].partition("/hub/api")[0] - + server_model["url"] - + "api", - verify=pebble_acme_ca_cert, - ) + + hub_parent_url = request_data["hub_url"].partition("/hub/api")[0] + + if CI_SUBDOMAIN_HOST: + # We can't make a proper request since wildcard DNS isn't setup, + # but we can set the Host header to test that CHP correctly forwards + # the request to the singleuser server + assert ( + server_model["url"] + == f"https://{jupyter_user}.{CI_SUBDOMAIN_HOST}/user/{jupyter_user}/" + ) + + # It shouldn't be possible to access the server without the subdomain, + # should instead be redirected to hub + r_incorrect = requests.get( + f"{hub_parent_url}/user/{jupyter_user}/api", + verify=pebble_acme_ca_cert, + allow_redirects=False, + ) + assert r_incorrect.status_code == 302 + + r = requests.get( + f"{hub_parent_url}/user/{jupyter_user}/api", + headers={"Host": f"{jupyter_user}.{CI_SUBDOMAIN_HOST}"}, + verify=False, + allow_redirects=False, + ) + else: + r = requests.get( + hub_parent_url + server_model["url"] + "api", + verify=pebble_acme_ca_cert, + ) + assert r.status_code == 200 assert "version" in r.json()