hub: extraEnv: JUPYTERHUB_CRYPT_KEY: valueFrom: secretKeyRef: name: jupyterhub-crypt-key key: crypt-key JUPYTERHUB_ADMIN_SERVICE_TOKEN: valueFrom: secretKeyRef: name: jupyterhub-admin-service-token key: token VAULT_ADDR: {{ .Env.VAULT_ADDR | quote }} NOTEBOOK_VAULT_TOKEN_TTL: {{ .Env.NOTEBOOK_VAULT_TOKEN_TTL | quote }} NOTEBOOK_VAULT_TOKEN_MAX_TTL: {{ .Env.NOTEBOOK_VAULT_TOKEN_MAX_TTL | quote }} {{- if eq .Env.JUPYTERHUB_VAULT_INTEGRATION_ENABLED "true" }} # Vault Agent provides renewable token via file (unlimited max TTL) VAULT_TOKEN_FILE: "/vault/secrets/vault-token" {{- end }} # Install packages at container startup extraFiles: startup.sh: mountPath: /usr/local/bin/startup.sh mode: 0755 stringData: | #!/bin/bash pip install --no-cache-dir hvac==2.3.0 exec jupyterhub --config /usr/local/etc/jupyterhub/jupyterhub_config.py --upgrade-db {{- if .Env.USER_POLICY_HCL }} user_policy.hcl: mountPath: /srv/jupyterhub/user_policy.hcl mode: 0644 stringData: | {{ .Env.USER_POLICY_HCL | strings.Indent 8 }} {{- end }} pre_spawn_hook.py: mountPath: /srv/jupyterhub/pre_spawn_hook.py mode: 0644 stringData: | {{ file.Read "pre_spawn_hook.py" | strings.Indent 8 }} # Override the default command to run our startup script first command: - /usr/local/bin/startup.sh config: JupyterHub: authenticator_class: generic-oauth admin_access: false authenticate_prometheus: false Authenticator: enable_auth_state: true allow_all: true # allow all Keycloak users GenericOAuthenticator: client_id: {{ .Env.JUPYTERHUB_OIDC_CLIENT_ID }} oauth_callback_url: "https://{{ .Env.JUPYTERHUB_HOST }}/hub/oauth_callback" authorize_url: "https://{{ .Env.KEYCLOAK_HOST }}/realms/{{ .Env.KEYCLOAK_REALM }}/protocol/openid-connect/auth" token_url: "https://{{ .Env.KEYCLOAK_HOST }}/realms/{{ .Env.KEYCLOAK_REALM }}/protocol/openid-connect/token" userdata_url: "https://{{ .Env.KEYCLOAK_HOST }}/realms/{{ .Env.KEYCLOAK_REALM }}/protocol/openid-connect/userinfo" login_service: keycloak # username_claim: email username_claim: preferred_username auth_refresh_age: 300 # Refresh auth token every 5 minutes refresh_pre_spawn: true # Refresh token before spawning server OAuthenticator: scope: - openid - profile - email extraConfig: load-pre-spawn-hook: | # Load pre_spawn_hook from external file with open('/srv/jupyterhub/pre_spawn_hook.py', 'r') as f: exec(f.read()) configure-security-context: | # Configure container security context for restricted Pod Security Standard c.KubeSpawner.container_security_context = { 'capabilities': { 'drop': ['ALL'] } } admin-service: | # Admin service for token management via API (used by get-token recipe) import os admin_token = os.environ.get('JUPYTERHUB_ADMIN_SERVICE_TOKEN', '') if admin_token: c.JupyterHub.services = [ { 'name': 'admin-service', 'api_token': admin_token, } ] c.JupyterHub.load_roles = [ { 'name': 'admin-service-role', 'scopes': ['admin:users', 'tokens', 'admin:servers'], 'services': ['admin-service'], } ] {{- if eq .Env.JUPYTERHUB_VAULT_INTEGRATION_ENABLED "true" }} # Vault token renewal sidecar configuration extraVolumes: - name: vault-secrets emptyDir: {} - name: vault-config configMap: name: vault-token-renewer-config - name: vault-admin-token secret: secretName: jupyterhub-vault-token extraVolumeMounts: - name: vault-secrets mountPath: /vault/secrets - name: vault-config mountPath: /vault/config - name: vault-admin-token mountPath: /vault/admin-token readOnly: true extraContainers: - name: vault-token-renewer image: hashicorp/vault:1.17.5 securityContext: runAsUser: 100 runAsGroup: 101 runAsNonRoot: true allowPrivilegeEscalation: false readOnlyRootFilesystem: false capabilities: drop: - ALL command: - /bin/sh - -c - | # Start token renewal script (handles both retrieval and renewal) exec sh /vault/config/vault-token-renewer.sh env: - name: VAULT_ADDR value: {{ .Env.VAULT_ADDR | quote }} - name: JUPYTERHUB_VAULT_TOKEN_TTL value: {{ .Env.JUPYTERHUB_VAULT_TOKEN_TTL | quote }} volumeMounts: - name: vault-secrets mountPath: /vault/secrets - name: vault-config mountPath: /vault/config - name: vault-admin-token mountPath: /vault/admin-token readOnly: true resources: requests: cpu: 50m memory: 64Mi limits: cpu: 100m memory: 128Mi {{- end }} podSecurityContext: fsGroup: {{ .Env.JUPYTER_FSGID }} networkPolicy: ingress: - from: - podSelector: matchLabels: hub.jupyter.org/network-access-hub: "true" - namespaceSelector: matchLabels: kubernetes.io/metadata.name: {{ .Env.PROMETHEUS_NAMESPACE }} ports: - port: http protocol: TCP proxy: service: type: ClusterIP singleuser: # Disable block-cloud-metadata sidecar for restricted Pod Security Standard compliance # Not needed in self-hosted environments without cloud metadata services cloudMetadata: blockWithIptables: false # Pod Security Standard (restricted) compliance allowPrivilegeEscalation: false # Additional security context via extraPodConfig {{- if eq .Env.JUPYTERHUB_GPU_ENABLED "true" }} extraPodConfig: runtimeClassName: nvidia securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault extraResource: limits: nvidia.com/gpu: "{{ .Env.JUPYTERHUB_GPU_LIMIT }}" {{- else }} extraPodConfig: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault {{- end }} extraEnv: VAULT_ADDR: "{{ .Env.VAULT_ADDR }}" NOTEBOOK_VAULT_TOKEN_TTL: "{{ .Env.NOTEBOOK_VAULT_TOKEN_TTL }}" NOTEBOOK_VAULT_TOKEN_MAX_TTL: "{{ .Env.NOTEBOOK_VAULT_TOKEN_MAX_TTL }}" {{- if eq .Env.JUPYTER_MCP_SERVER_ENABLED "true" }} # Enable WebSocket token authentication for jupyter-mcp-server extension # https://github.com/datalayer/jupyter-mcp-server/issues/61 JUPYTERHUB_ALLOW_TOKEN_IN_URL: "1" {{- end }} storage: {{ if env.Getenv "PVC_NAME" -}} type: static static: pvcName: {{ .Env.PVC_NAME }} {{ else -}} type: dynamic dynamic: {{ if env.Getenv "JUPYTERHUB_STORAGE_CLASS" -}} storageClass: {{ .Env.JUPYTERHUB_STORAGE_CLASS }} {{ end -}} storageAccessModes: - ReadWriteOnce {{ end -}} capacity: 10Gi {{- if eq .Env.JUPYTERHUB_AIRFLOW_DAGS_PERSISTENCE_ENABLED "true" }} extraVolumes: - name: airflow-dags persistentVolumeClaim: claimName: airflow-dags-pvc extraVolumeMounts: - name: airflow-dags mountPath: /home/jovyan/airflow-dags readOnly: false {{- end }} networkPolicy: egress: {{- if eq .Env.JUPYTERHUB_AIRFLOW_DAGS_PERSISTENCE_ENABLED "true" }} # Allow communication with Airflow API server in the same namespace - to: - podSelector: matchLabels: release: airflow component: api-server ports: - port: 8080 protocol: TCP {{- end }} - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: postgres ports: - port: 5432 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: qdrant ports: - port: 6333 protocol: TCP - port: 6334 protocol: TCP - port: 6335 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: litellm ports: - port: 4000 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: vault ports: - port: 8200 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: clickhouse ports: - port: 8123 protocol: TCP - port: 9000 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: airflow ports: - port: 8080 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: minio ports: - port: 9000 protocol: TCP - port: 9001 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: lakekeeper ports: - port: 8181 protocol: TCP # Allow communication with FalkorDB - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: falkordb ports: - port: 6379 protocol: TCP # Allow DNS resolution - to: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 53 protocol: UDP - port: 53 protocol: TCP # Allow HTTP traffic - to: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 80 protocol: TCP # Allow HTTPS traffic - to: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 443 protocol: TCP image: pullPolicy: IfNotPresent profileList: # https://quay.io/repository/jupyter/pyspark-notebook {{- if eq .Env.JUPYTER_PROFILE_MINIMAL_ENABLED "true" }} - display_name: "Minimal Jupyter Notebook Stack" description: "Minimal Jupyter Notebook Stack" kubespawner_override: image: quay.io/jupyter/minimal-notebook {{- end }} {{ if eq .Env.JUPYTER_PROFILE_BASE_ENABLED "true" }} - display_name: "Base Jupyter Notebook Stack" description: "Base Jupyter Notebook Stack" kubespawner_override: image: quay.io/jupyter/base-notebook {{- end }} {{- if eq .Env.JUPYTER_PROFILE_DATASCIENCE_ENABLED "true" }} - display_name: "Jupyter Notebook Data Science Stack" description: "Jupyter Notebook Data Science Stack" kubespawner_override: image: quay.io/jupyter/datascience-notebook {{- end }} {{- if eq .Env.JUPYTER_PROFILE_PYSPARK_ENABLED "true" }} - display_name: "Jupyter Notebook Python, Spark Stack" description: "Jupyter Notebook Python, Spark Stack" kubespawner_override: image: quay.io/jupyter/pyspark-notebook {{- end }} {{- if eq .Env.JUPYTER_PROFILE_PYTORCH_ENABLED "true" }} - display_name: "Jupyter Notebook PyTorch Deep Learning Stack" description: "Jupyter Notebook PyTorch Deep Learning Stack" kubespawner_override: image: quay.io/jupyter/pytorch-notebook {{- end }} {{- if eq .Env.JUPYTER_PROFILE_TENSORFLOW_ENABLED "true" }} - display_name: "Jupyter Notebook TensorFlow Deep Learning Stack" description: "Jupyter Notebook TensorFlow Deep Learning Stack" kubespawner_override: image: quay.io/jupyter/tensorflow-notebook {{- end }} {{- if eq .Env.JUPYTER_PROFILE_BUUN_STACK_ENABLED "true" }} - display_name: "Buun-stack" description: "Jupyter Notebook with buun-stack" kubespawner_override: image: "{{ .Env.IMAGE_REGISTRY }}/{{ .Env.KERNEL_IMAGE_BUUN_STACK_REPOSITORY }}:{{ .Env.JUPYTER_PYTHON_KERNEL_TAG }}" {{- end }} {{- if eq .Env.JUPYTER_PROFILE_BUUN_STACK_CUDA_ENABLED "true" }} - display_name: "Buun-stack with CUDA" description: "Jupyter Notebook with buun-stack and CUDA support" kubespawner_override: image: "{{ .Env.IMAGE_REGISTRY }}/{{ .Env.KERNEL_IMAGE_BUUN_STACK_CUDA_REPOSITORY }}:{{ .Env.JUPYTER_PYTHON_KERNEL_TAG }}" # resources: # requests: # nvidia.com/gpu: "1" {{- end }} cull: enabled: true # for production timeout: 7200 # 2 hours idle timeout every: 600 # Check every 10 minutes # for testing # timeout: 300 # 5 minutes idle timeout (for testing) │ │ # every: 60 # Check every 1 minute (for testing) │ │ # Maximum age of a server pod before forced restart # IMPORTANT: This must be less than NOTEBOOK_VAULT_TOKEN_MAX_TTL to prevent token expiry # - NOTEBOOK_VAULT_TOKEN_MAX_TTL: {{ .Env.NOTEBOOK_VAULT_TOKEN_MAX_TTL }} (7 days = 604800s) # - JUPYTERHUB_CULL_MAX_AGE: {{ .Env.JUPYTERHUB_CULL_MAX_AGE }}s (6 days = 518400s) # Pod restart creates new user token, preventing 7-day token expiry maxAge: {{ .Env.JUPYTERHUB_CULL_MAX_AGE }} adminUsers: true # Also cull admin users' server pods users: false # Don't delete user accounts, only stop server pods # imagePullSecrets: # - name: regcred ingress: enabled: true annotations: kubernetes.io/ingress.class: traefik traefik.ingress.kubernetes.io/router.entrypoints: websecure ingressClassName: traefik hosts: - {{ .Env.JUPYTERHUB_HOST }} pathType: Prefix tls: - hosts: - {{ .Env.JUPYTERHUB_HOST }}