Skip to content

Commit 8996e92

Browse files
committed
keycloak notes
1 parent 4278254 commit 8996e92

File tree

1 file changed

+247
-1
lines changed

1 file changed

+247
-1
lines changed

modules/tutorials/pages/jupyterhub.adoc

Lines changed: 247 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,250 @@
22
:description: A tutorial on how to configure various aspects of JupyterHub on Kubernetes.
33
:keywords: notebook, JupyterHub, Kubernetes, k8s, Spark, HDFS, S3
44

5-
This tutorial illustrates various configuration settings when using JupyterHub on Kubernetes.
5+
.Drop-down example
6+
[%collapsible]
7+
====
8+
xxx:
9+
10+
[source,console]
11+
----
12+
xxx
13+
----
14+
====
15+
16+
This tutorial illustrates various scenarios and configuration options when using JupyterHub on Kubernetes.
17+
The custom resources and configuration settings that are discussed here are based on the xref:demos:jupyterhub-keycloak.adoc[JupyterHub-Keycloak demo], so you may find it helpful to have that demo running to reference things as you read through this tutorial.
18+
19+
== Keycloak
20+
21+
Keycloak is installed using a https://github.com/stackabletech/demos/blob/feat/keycloak-jupyterhub/stacks/jupyterhub-keycloak/keycloak.yaml[Deployment] that loads realm configuration mounted as a ConfigMap.
22+
23+
=== Services
24+
25+
In the demo, the keycloak and jupyter hub service (proxy-public) ports are fixed e.g.
26+
27+
[source,yaml]
28+
---
29+
apiVersion: v1
30+
kind: Service
31+
metadata:
32+
name: keycloak
33+
labels:
34+
app: keycloak
35+
spec:
36+
type: NodePort
37+
selector:
38+
app: keycloak
39+
ports:
40+
- name: https
41+
port: 8443
42+
targetPort: 8443
43+
nodePort: 31093 # <1>
44+
----
45+
46+
<1> Static value for the purposed of the demo.
47+
48+
They are:
49+
50+
- `31093` for keycloak
51+
- `31095` for jupyterhub/proxy-public
52+
53+
The keycloak and jupyterhub endpoints are defined in the jupyter hub chart values i.e. for the purposes of the demo (that does not use any pre-defined DNS settings), the ports have to be known before the jupyter hub components are deployed.
54+
55+
This can be achieved by having the keycloak deployment write out its co-ordinates into a ConfigMap during start-up, which can then be referenced by the JupyterHub chart like this:
56+
57+
[source,yaml]
58+
---
59+
options:
60+
hub:
61+
config:
62+
...
63+
extraEnv:
64+
...
65+
KEYCLOAK_NODEPORT_URL:
66+
valueFrom:
67+
configMapKeyRef:
68+
name: keycloak-address
69+
key: keycloakAddress # <1>
70+
KEYCLOAK_NODE_IP:
71+
valueFrom:
72+
configMapKeyRef:
73+
name: keycloak-address
74+
key: keycloakNodeIp
75+
...
76+
extraConfig:
77+
...
78+
03-set-endpoints: |
79+
import os
80+
from oauthenticator.generic import GenericOAuthenticator
81+
keycloak_url = os.getenv("KEYCLOAK_NODEPORT_URL") # <2>
82+
...
83+
keycloak_node_ip = os.getenv("KEYCLOAK_NODE_IP")
84+
...
85+
c.GenericOAuthenticator.oauth_callback_url: f"http://{keycloak_node_ip}:31095/hub/oauth_callback" # <3>
86+
c.GenericOAuthenticator.authorize_url = f"https://{keycloak_url}/realms/demo/protocol/openid-connect/auth"
87+
c.GenericOAuthenticator.token_url = f"https://{keycloak_url}/realms/demo/protocol/openid-connect/token"
88+
c.GenericOAuthenticator.userdata_url = f"https://{keycloak_url}/realms/demo/protocol/openid-connect/userinfo"
89+
----
90+
91+
<1> endpoint information read from the ConfigMap
92+
<2> this information is passed to a variable in one of the start-up config scripts...
93+
<3> ...and then used for JupyterHub settings
94+
95+
=== Discovery
96+
97+
As mentioned above, keycloak writes out it endpoint information to ConfigMap, like this:
98+
99+
[source,yaml]
100+
----
101+
---
102+
apiVersion: apps/v1
103+
kind: Deployment
104+
...
105+
spec:
106+
containers:
107+
...
108+
- name: create-configmap
109+
resources: {}
110+
image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev
111+
command: ["/bin/bash", "-c"]
112+
args:
113+
- |
114+
pid=
115+
trap 'echo SIGINT; [[ $pid ]] && kill $pid; exit' SIGINT
116+
trap 'echo SIGTERM; [[ $pid ]] && kill $pid; exit' SIGTERM
117+
118+
while :
119+
do
120+
echo "Determining Keycloak public reachable address"
121+
KEYCLOAK_ADDRESS=$(kubectl get svc keycloak -o json | jq -r --argfile endpoints <(kubectl get endpoints keycloak -o json) --argfile nodes <(kubectl get nodes -o json) '($nodes.items[] | select(.metadata.name == $endpoints.subsets[].addresses[].nodeName) | .status.addresses | map(select(.type == "ExternalIP" or .type == "InternalIP")) | min_by(.type) | .address | tostring) + ":" + (.spec.ports[] | select(.name == "https") | .nodePort | tostring)')
122+
echo "Found Keycloak running at $KEYCLOAK_ADDRESS"
123+
124+
if [ ! -z "$KEYCLOAK_ADDRESS" ]; then
125+
KEYCLOAK_HOSTNAME="$(echo $KEYCLOAK_ADDRESS | grep -oP '^[^:]+')"
126+
KEYCLOAK_PORT="$(echo $KEYCLOAK_ADDRESS | grep -oP '[0-9]+$')"
127+
128+
cat << EOF | kubectl apply -f -
129+
apiVersion: v1
130+
kind: ConfigMap
131+
metadata:
132+
name: keycloak-address
133+
data:
134+
keycloakAddress: "$KEYCLOAK_HOSTNAME:$KEYCLOAK_PORT"
135+
keycloakNodeIp: "$KEYCLOAK_HOSTNAME"
136+
EOF
137+
fi
138+
139+
sleep 30 & pid=$!
140+
wait
141+
done
142+
----
143+
144+
=== Security
145+
146+
We create a keystore with a self-generated and self-signed certificate and mount it so that the keystore file can be used when starting keycloak:
147+
148+
[source,yaml]
149+
----
150+
spec:
151+
containers:
152+
- name: keycloak
153+
...
154+
args:
155+
- start
156+
- --hostname-strict=false
157+
- --https-key-store-file=/tls/keystore.p12 # <3>
158+
- --https-key-store-password=changeit
159+
- --import-realm
160+
volumeMounts:
161+
- name: tls
162+
mountPath: /tls/ # <2>
163+
...
164+
volumes:
165+
...
166+
- name: tls
167+
ephemeral:
168+
volumeClaimTemplate:
169+
metadata:
170+
annotations:
171+
secrets.stackable.tech/class: tls # <1>
172+
secrets.stackable.tech/format: tls-pkcs12
173+
secrets.stackable.tech/format.compatibility.tls-pkcs12.password: changeit
174+
secrets.stackable.tech/scope: service=keycloak,node
175+
spec:
176+
storageClassName: secrets.stackable.tech
177+
accessModes:
178+
- ReadWriteOnce
179+
resources:
180+
requests:
181+
storage: "1"
182+
----
183+
184+
<1> Create a volume holding the self-signed certificate information
185+
<2> Mount this volume for keycloak to use
186+
<3> Pass the keystore file as an argument on start-up
187+
188+
For the self-signed certificate to be accepted during the handshake between JupyterHub and Keycloak it is important to create the jupyterhub-side certificate using the same secret class, although the format can be a different one:
189+
190+
[source,yaml]
191+
----
192+
extraVolumes:
193+
- name: tls-ca-cert
194+
ephemeral:
195+
volumeClaimTemplate:
196+
metadata:
197+
annotations:
198+
secrets.stackable.tech/class: tls
199+
spec:
200+
storageClassName: secrets.stackable.tech
201+
accessModes:
202+
- ReadWriteOnce
203+
resources:
204+
requests:
205+
storage: "1"
206+
----
207+
208+
=== Realm
209+
210+
The Keycloak https://github.com/stackabletech/demos/blob/feat/keycloak-jupyterhub/stacks/jupyterhub-keycloak/keycloak-realm-config.yaml for the demo basically contains a set of users and groups, along with a simple client definition:
211+
212+
[source,yaml]
213+
----
214+
"clients" : [ {
215+
"clientId": "jupyterhub",
216+
"enabled": true,
217+
"protocol": "openid-connect",
218+
"clientAuthenticatorType": "client-secret",
219+
"secret": ...,
220+
"redirectUris" : [ "*" ],
221+
"webOrigins" : [ "*" ],
222+
"standardFlowEnabled": true
223+
} ]
224+
----
225+
226+
Not that the standard flow is enabled and no other OAuth-specific settings are required.
227+
Wildcards are used for redirectUris and webOrigins, mainly for the sake of simplicity: in production environments this would typically be limited or filtered in an appropriate way.
228+
229+
== JupyterHub
230+
231+
=== Authentication
232+
233+
==== Native Authenticator
234+
235+
==== OAuth Authenticator (Keycloak)
236+
237+
=== Certificates
238+
239+
=== Driver Service
240+
241+
=== Endpoints
242+
243+
=== Profiles
244+
245+
== Images
246+
247+
== Example Notebook
248+
249+
=== Provisos
250+
251+
=== Overview

0 commit comments

Comments
 (0)