Skip to content

Seed secrets (proxy.secretToken, etc) so they don't have to be manually generated #1993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions .github/workflows/test-chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,15 @@ jobs:
- k3s-channel: v1.19
test: upgrade
upgrade-from: stable
upgrade-from-extra-args: >-
--set proxy.secretToken=aaa111
--set hub.cookieSecret=bbb222
--set hub.config.CryptKeeper.keys[0]=ccc333
- k3s-channel: v1.19
test: upgrade
upgrade-from: dev
upgrade-from-extra-args: >-
--set proxy.secretToken=aaa111

steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -163,10 +169,11 @@ jobs:
#
# https://github.com/helm/helm/issues/9244
cd ci
helm install jupyterhub --repo https://jupyterhub.github.io/helm-chart/ jupyterhub --values ../dev-config.yaml --version=$UPGRADE_FROM_VERSION
helm install jupyterhub --repo https://jupyterhub.github.io/helm-chart/ jupyterhub --values ../dev-config.yaml --version=$UPGRADE_FROM_VERSION ${{ matrix.upgrade-from-extra-args }}

echo ""
echo "Installing Helm diff plugin while k8s resources are initializing"
- name: "Install helm diff plugin"
if: matrix.test == 'upgrade'
run: |
helm plugin install https://github.com/databus23/helm-diff

# ref: https://github.com/jacobtomlinson/gha-read-helm-chart
Expand All @@ -190,6 +197,7 @@ jobs:
echo

helm diff upgrade --install jupyterhub ./jupyterhub --values dev-config.yaml \
--show-secrets \
--context=3 \
--post-renderer=ci/string-replacer.sh

Expand All @@ -203,8 +211,11 @@ jobs:

- name: "Install or upgrade to local chart"
run: |
. ./ci/common
helm upgrade --install jupyterhub ./jupyterhub --values dev-config.yaml

- name: Await local chart
run: |
. ./ci/common
await_jupyterhub
await_autohttps_tls_cert_acquisition

Expand Down
2 changes: 0 additions & 2 deletions dev-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
proxy:
secretToken: pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
service:
type: NodePort
nodePorts:
Expand Down Expand Up @@ -33,7 +32,6 @@ proxy:
egress: [] # overrides allowance of 0.0.0.0/0

hub:
cookieSecret: cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
db:
type: sqlite-memory
services:
Expand Down
49 changes: 2 additions & 47 deletions doc/source/administrator/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Now we are sure that something is wrong with our Dockerfile. Let's check
our `config.yaml` file for the section where we specify the user's
Docker image. Here we see our problem:

```
```yaml
singleuser:
image:
name: jupyter/scipy-notebook
Expand All @@ -116,7 +116,7 @@ the pod to fail.

To fix this, let's add a tag to our `config.yaml` file:

```
```yaml
singleuser:
image:
name: jupyter/scipy-notebook
Expand Down Expand Up @@ -173,48 +173,3 @@ And now we see that we have a running user pod!
Note that many debugging situations are not as straightforward as this one.
It will take some time before you get a feel for the errors that Kubernetes
may throw at you, and how these are tied to your configuration files.

## Troubleshooting Examples

The following sections contain some case studies that illustrate some of the
more common bugs / gotchas that you may experience using JupyterHub with
Kubernetes.

### Hub fails to start

**Symptom:** following `kubectl get pod`, the `hub` pod is in
`Error` or `CrashLoopBackoff` state, or appears to be running but accessing
the website for the JupyterHub returns an error message in the browser).

**Investigating:** the output of `kubectl --namespace=jhub logs hub...` shows something like:

```
File "/usr/local/lib/python3.5/dist-packages/jupyterhub/proxy.py", line 589, in get_all_routes
resp = yield self.api_request('', client=client)
tornado.httpclient.HTTPError: HTTP 403: Forbidden
```

**Diagnosis:** This is likely because the `hub` pod cannot
communicate with the proxy pod API, likely because of a problem in the
`secretToken` that was put in `config.yaml`.

**Fix:** Follow these steps:

1. Create a secret token:

```
openssl rand -hex 32
```

2. Add the token to `config.yaml` like so:

```
proxy:
secretToken: '<output of `openssl rand -hex 32`>'
```

3. Redeploy the helm chart:

```
helm upgrade --cleanup-on-fail jhub jupyterhub/jupyterhub -f config.yaml
```
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an open issue to improve the debugging section, so instead of finding an example with diagnosis etc that wasn't correct any more, I opted to delete the section.

87 changes: 39 additions & 48 deletions doc/source/jupyterhub/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,45 @@

# Installing JupyterHub

Now that we have a {doc}`Kubernetes cluster </kubernetes/setup-kubernetes>` and {doc}`Helm </kubernetes/setup-helm>` setup, we can proceed by using Helm to install JupyterHub
and related {term}`Kubernetes resources <Kubernetes resource>` using a
{term}`Helm chart`.

## Prepare configuration file

In this step we will prepare a [YAML](https://en.wikipedia.org/wiki/YAML)
configuration file that we will refer to as `config.yaml`. It will contain the multiple
{term}`Helm values` to be provided to a JupyterHub {term}`Helm chart` developed
specifically together with this guide.

Helm charts contains {term}`templates <Helm template>` that with provided values will render to {term}`Kubernetes resources <Kubernetes resource>` to be installed in a Kubernetes cluster. This
config file will provide the values to be used by our Helm chart.

1. Generate a random hex string representing 32 bytes to use as a security
token. Run this command in a terminal and copy the output:

```{code-block} bash

openssl rand -hex 32

```

2. Create and start editing a file called `config.yaml`. In the code snippet
below we start the widely available [nano editor](https://en.wikipedia.org/wiki/GNU_nano), but any editor will do.

```
nano config.yaml
```

3. Write the following into the `config.yaml` file but instead of writing
`<RANDOM-HEX>` paste the generated hex string you copied in step 1.

```
proxy:
secretToken: "<RANDOM_HEX>"
```

It is common practice for Helm and Kubernetes YAML files to indent using
two spaces.

4. Save the `config.yaml` file. In the nano editor this is done by pressing **CTRL+X** or
**CMD+X** followed by a confirmation to save the changes.

<!---
Don't put an example here! People will just copy paste that & that's a
security issue.
-->
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were significant differences here, so I ended up updating some leading paragraphs as well.

With a {doc}`Kubernetes cluster </kubernetes/setup-kubernetes>` cluster
available and {doc}`Helm </kubernetes/setup-helm>` installed, we can install
JupyterHub in the Kubernetes cluster using the JupyterHub Helm chart.

## Initialize a Helm chart configuration file

Helm charts' contain {term}`templates <Helm template>` that can be rendered to
the {term}`Kubernetes resources <Kubernetes resource>` to be installed. A user
of a Helm chart can override the chart's default values to influence how the
templates render.

In this step we will initialize a chart configuration file for you to adjust
your installation of JupyterHub. We will name and refer to it as `config.yaml`
going onwards.

```{admonition} Introduction to YAML
If you haven't worked with YAML before, investing some
minutes [learning about it]([YAML](https://www.youtube.com/watch?v=cdLNKUoMc6c)
will likely be worth your time.
```

As of version 1.0.0, you don't need any configuration to get started so you can
just create a `config.yaml` file with some helpful comments.

```yaml
# This file can update the JupyterHub Helm chart's default configuration values.
#
# For reference see the configuration reference and default values, but make
# sure to refer to the Helm chart version of interest to you!
#
# Introduction to YAML: https://www.youtube.com/watch?v=cdLNKUoMc6c
# Chart config reference: https://zero-to-jupyterhub.readthedocs.io/en/stable/resources/reference.html
# Chart default values: https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/HEAD/jupyterhub/values.yaml
# Available chart versions: https://jupyterhub.github.io/helm-chart/
#
```

In case you are working from a terminal and are unsure how to create this file,
can try with `nano config.yaml`.

## Install JupyterHub

Expand Down
28 changes: 19 additions & 9 deletions jupyterhub/files/hub/jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
configuration_directory = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, configuration_directory)

from z2jh import get_config, set_config_if_not_none, get_name, get_name_env
from z2jh import (
get_config,
set_config_if_not_none,
get_name,
get_name_env,
get_secret_value,
)


def camelCaseify(s):
Expand Down Expand Up @@ -66,7 +72,6 @@ def camelCaseify(s):
("concurrent_spawn_limit", None),
("active_server_limit", None),
("base_url", None),
# ('cookie_secret', None), # requires a Hex -> Byte transformation
("allow_named_servers", None),
("named_server_limit_per_user", None),
("authenticate_prometheus", None),
Expand All @@ -79,11 +84,6 @@ def camelCaseify(s):
cfg_key = camelCaseify(trait)
set_config_if_not_none(c.JupyterHub, trait, "hub." + cfg_key)

# a required Hex -> Byte transformation
cookie_secret_hex = get_config("hub.cookieSecret")
if cookie_secret_hex:
c.JupyterHub.cookie_secret = a2b_hex(cookie_secret_hex)

# hub_bind_url configures what the JupyterHub process within the hub pod's
# container should listen to.
hub_container_port = 8081
Expand Down Expand Up @@ -372,9 +372,19 @@ def camelCaseify(s):
c.Spawner.debug = True


# load hub.config values
# load potentially seeded secrets
c.JupyterHub.proxy_auth_token = get_secret_value("JupyterHub.proxy_auth_token")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify: the change here is that these secrets are no longer optional in the chart, and cannot possibly be left unspecified or null, is that correct? The previous logic allowed these to be unspecified, in which case the existing default behavior would occur, but explicitly setting them to null or empty values is not the same thing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is correct that we will now always provide a value to these three configuration options, if you search for "assert hack" you will find an assertion I make about that.

Any falsy value for proxy.secretToken, hub.cookieSecret, hub.config.CryptKeeper.keys would lead to automatic generation of a new passwords.

Is this indirectly influencing the behavior of JupyterHub?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_secret_value("JupyterHub.proxy_auth_token") returns the hub k8s Secret's key's value, which in turn is set by a named template, which in turn has priority logic to:

  1. return any explicitly set value
  2. return the previous k8s Secret's value
  3. return autogenerated value

c.JupyterHub.cookie_secret = a2b_hex(get_secret_value("JupyterHub.cookie_secret"))
c.CryptKeeper.keys = get_secret_value("CryptKeeper.keys").split(";")

# load hub.config values, except potentially seeded secrets
for section, sub_cfg in get_config("hub.config", {}).items():
c[section].update(sub_cfg)
if section == "JupyterHub" and sub_cfg in ["proxy_auth_token", "cookie_secret"]:
pass
elif section == "CryptKeeper" and sub_cfg in ["keys"]:
pass
else:
c[section].update(sub_cfg)

# execute hub.extraConfig string
extra_config = get_config("hub.extraConfig", {})
Expand Down
12 changes: 12 additions & 0 deletions jupyterhub/files/hub/z2jh.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ def _get_config_value(key):
raise Exception(f"{path} not found!")


@lru_cache()
def get_secret_value(key):
"""Load value from the k8s Secret given a key."""

path = f"/etc/jupyterhub/secret/{key}"
if os.path.exists(path):
with open(path) as f:
return f.read()
else:
raise Exception(f"{path} not found!")


def get_name(name):
"""Returns the fullname of a resource given its short name"""
return _get_config_value(name)
Expand Down
26 changes: 11 additions & 15 deletions jupyterhub/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ properties:
- string
- "null"
description: |
```{note}
As of version 1.0.0 this will automatically be generated and there is
no need to set it manually.
```

A 32-byte cryptographically secure randomly generated string used to sign values of
secure cookies set by the hub. If unset, jupyterhub will generate one on startup and
save it in the file `jupyterhub_cookie_secret` in the `/srv/jupyterhub` directory of
Expand Down Expand Up @@ -468,8 +473,6 @@ properties:
The user specified in the connection string must have the rights to create
tables in the database specified.

Note that if you use this, you *must* also set `hub.cookieSecret`.

4. **postgres**

Use an externally hosted postgres database.
Expand All @@ -484,8 +487,6 @@ properties:

The user specified in the connection string must have the rights to create
tables in the database specified.

Note that if you use this, you *must* also set `hub.cookieSecret`.
pvc:
type: object
description: |
Expand Down Expand Up @@ -717,16 +718,6 @@ properties:
This k8s Secret must represent the structure generated by this chart
and by using this option, you are in change of ensuring the secret
structure is reflected when upgrading to new versions of the chart.

```yaml
apiVersion: v1
data:
proxy.token: < FILL IN >
values.yaml: < FILL IN >
kind: Secret
metadata:
name: my-self-managed-secret
```
nodeSelector: &nodeSelector-spec
type:
- object
Expand Down Expand Up @@ -836,6 +827,11 @@ properties:
secretToken:
type: string
description: |
```{note}
As of version 1.0.0 this will automatically be generated and there is
no need to set it manually.
```

A 32-byte cryptographically secure randomly generated string used to secure communications
between the hub and the configurable-http-proxy.

Expand All @@ -846,7 +842,7 @@ properties:

Changing this value will cause the proxy and hub pods to restart. It is good security
practice to rotate these values over time. If this secret leaks, *immediately* change
it to something else, or user data can be compromised
it to something else, or user data can be compromised.
service:
type: object
description: |
Expand Down
Loading