Skip to content

Commit 014de3c

Browse files
authored
Merge pull request #3548 from manics/subdomain-host
Support subdomain_host (CHP needs --host-routing)
2 parents 26eb8ef + dcd031a commit 014de3c

File tree

5 files changed

+106
-8
lines changed

5 files changed

+106
-8
lines changed

.github/workflows/test-chart.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,14 @@ jobs:
137137
--set hub.image.name=quay.io/jupyterhub/k8s-hub-slim
138138
--set prePuller.hook.enabled=true
139139
--set prePuller.hook.pullOnlyOnChanges=true
140-
- k3s-channel: v1.31 # also test hub.existingSecret
140+
- k3s-channel: v1.31 # also test hub.existingSecret and subdomain_host
141141
test: install
142142
local-chart-extra-args: >-
143143
--set hub.existingSecret=test-hub-existing-secret
144144
--set proxy.secretToken=aaaa1111
145145
--set hub.cookieSecret=bbbb2222
146146
--set hub.config.CryptKeeper.keys[0]=cccc3333
147+
--set hub.config.JupyterHub.subdomain_host=jupyterhub.example.org
147148
create-k8s-test-resources: true
148149

149150
# We run three upgrade tests where we first install an already released
@@ -368,6 +369,9 @@ jobs:
368369
continue-on-error: ${{ matrix.accept-failure == true }}
369370
run: |
370371
. ./ci/common
372+
if [ "${{ contains(matrix.local-chart-extra-args, 'subdomain_host') }}" = "true" ]; then
373+
export CI_SUBDOMAIN_HOST=jupyterhub.example.org
374+
fi
371375
# If you have problems with the tests add '--capture=no' to show stdout
372376
pytest --verbose --maxfail=2 --color=yes ./tests
373377

docs/source/administrator/security.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,44 @@ proxy:
489489
```
490490
491491
This would restrict the access to only two IP addresses: `111.111.111.111` and `222.222.222.222`.
492+
493+
(jupyterhub_subdomains)=
494+
495+
## Host user servers on a subdomain
496+
497+
You can reduce the chance of cross-origin attacks by giving each user
498+
their own subdomain `<user>.jupyter.example.org`.
499+
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.
500+
501+
```yaml
502+
hub:
503+
config:
504+
JupyterHub:
505+
subdomain_host: jupyter.example.org
506+
```
507+
508+
If you are using a Kubernetes ingress this must include hosts
509+
`jupyter.example.org` and `*.jupyter.example.org`.
510+
For example:
511+
512+
```yaml
513+
ingress:
514+
enabled: true
515+
hosts:
516+
- jupyter.example.org
517+
- "*.jupyter.example.org"
518+
tls:
519+
- hosts:
520+
- jupyter.example.org
521+
- "*.jupyter.example.org"
522+
secretName: example-tls
523+
```
524+
525+
where `example-tls` is the name of a Kubernetes secret containing the wildcard certificate and key.
526+
527+
The chart does not support the automatic creation of wildcard HTTPS certificates.
528+
You must obtain a certificate from an external source,
529+
for example by using an ACME client such as [cert-manager with the DNS-01 challenge](https://cert-manager.io/docs/configuration/acme/dns01/),
530+
and ensure the certificate and key are stored in the secret.
531+
532+
See {ref}`jupyterhub:subdomains` in the JupyterHub documentation for more information.

jupyterhub/templates/proxy/deployment.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ spec:
100100
{{- if .Values.debug.enabled }}
101101
- --log-level=debug
102102
{{- end }}
103+
{{- if .Values.hub.config.JupyterHub.subdomain_host }}
104+
- --host-routing
105+
{{- end }}
103106
{{- range .Values.proxy.chp.extraCommandLineFlags }}
104107
- {{ tpl . $ }}
105108
{{- end }}

jupyterhub/values.schema.yaml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ properties:
217217
values, you need to use [`hub.extraConfig`](schema_hub.extraConfig)
218218
instead.
219219
220-
```{admonition} Currently intended only for auth config
220+
```{admonition} Some configuration must be set in multiple places
221221
:class: warning
222222
This config _currently_ (0.11.0) only influence the software in the
223223
`hub` Pod, but some Helm chart config options such as
@@ -271,6 +271,25 @@ properties:
271271
the `--values` or `-f` flag. During merging, lists are replaced while
272272
dictionaries are updated.
273273
```
274+
properties:
275+
JupyterHub:
276+
type: object
277+
additionalProperties: true
278+
description: |
279+
JupyterHub Traitlets configuration.
280+
281+
See {py:mod}`jupyterhub:jupyterhub.app` for the full list,
282+
but take note of the [above warnings](schema_hub.config).
283+
properties:
284+
subdomain_host:
285+
type: string
286+
description: |
287+
The subdomain to use for hosting singleuser servers.
288+
289+
This helps protect against some cross-origin attacks by giving each user
290+
their own subdomain `<user>.jupyter.example.org`.
291+
292+
See {ref}`jupyterhub_subdomains`.
274293
extraFiles: &extraFiles
275294
type: object
276295
additionalProperties: false

tests/test_spawn.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import json
2+
import os
23
import subprocess
34
import time
45

56
import pytest
67
import requests
78

9+
# If we're testing subdomain hosts in GitHub CI our workflow will set this
10+
CI_SUBDOMAIN_HOST = os.getenv("CI_SUBDOMAIN_HOST")
11+
812

913
def test_spawn_basic(
1014
api_request,
@@ -28,12 +32,39 @@ def test_spawn_basic(
2832
api_request, jupyter_user, request_data["test_timeout"]
2933
)
3034
assert server_model
31-
r = requests.get(
32-
request_data["hub_url"].partition("/hub/api")[0]
33-
+ server_model["url"]
34-
+ "api",
35-
verify=pebble_acme_ca_cert,
36-
)
35+
36+
hub_parent_url = request_data["hub_url"].partition("/hub/api")[0]
37+
38+
if CI_SUBDOMAIN_HOST:
39+
# We can't make a proper request since wildcard DNS isn't setup,
40+
# but we can set the Host header to test that CHP correctly forwards
41+
# the request to the singleuser server
42+
assert (
43+
server_model["url"]
44+
== f"https://{jupyter_user}.{CI_SUBDOMAIN_HOST}/user/{jupyter_user}/"
45+
)
46+
47+
# It shouldn't be possible to access the server without the subdomain,
48+
# should instead be redirected to hub
49+
r_incorrect = requests.get(
50+
f"{hub_parent_url}/user/{jupyter_user}/api",
51+
verify=pebble_acme_ca_cert,
52+
allow_redirects=False,
53+
)
54+
assert r_incorrect.status_code == 302
55+
56+
r = requests.get(
57+
f"{hub_parent_url}/user/{jupyter_user}/api",
58+
headers={"Host": f"{jupyter_user}.{CI_SUBDOMAIN_HOST}"},
59+
verify=False,
60+
allow_redirects=False,
61+
)
62+
else:
63+
r = requests.get(
64+
hub_parent_url + server_model["url"] + "api",
65+
verify=pebble_acme_ca_cert,
66+
)
67+
3768
assert r.status_code == 200
3869
assert "version" in r.json()
3970

0 commit comments

Comments
 (0)