I have a Jupyter Hub deployment and currently I am trying to set up internal SSL. However, I think I am having issues because of a custom service that I wrote. I’m a bit green when it comes to certificate configuration, so any help is appreciated.
Here is a snippet from my config file showing how I set up my service and how I have enabled internal SSL. :
c.JupyterHub.services = [
{
"name": "<my_website>",
"url": "https://127.0.0.1:8899",
"command": ["python3", "/opt/tljh/services/<my_website>_proxy.py"],
"oauth_no_confirm": True,
}
]
c.JupyterHub.trusted_alt_names = [
"DNS:<domain jupyter hub is being served at>",
"IP:127.0.0.1",
"DNS:localhost",
"DNS:<domain the website I am trying to proxy is served at>",
]
c.JupyterHub.internal_certs_location = "/etc/jupyterhub/internal-ssl"
c.JupyterHub.internal_ssl = True
The <my_website>_proxy.py runs a proxy to a private website through a Tornado HTTP server. I am using this in conjunction with custom JupyterLab Launcher Cards to embed the website in an i-frame with in the analysis environment.
When I run tljh-config reload, the certifcates are generated along with the JSON below that shows how they are related:
{
"hub-ca": {
"serial": 0,
"is_ca": true,
"parent_ca": "",
"signees": {
"hub-internal": 1
},
"files": {
"key": "/etc/jupyterhub/internal-ssl/hub-ca/hub-ca.key",
"cert": "/etc/jupyterhub/internal-ssl/hub-ca/hub-ca.crt",
"ca": ""
}
},
"notebooks-ca": {
"serial": 0,
"is_ca": true,
"parent_ca": "",
"signees": {
"user-service": 1
},
"files": {
"key": "/etc/jupyterhub/internal-ssl/notebooks-ca/notebooks-ca.key",
"cert": "/etc/jupyterhub/internal-ssl/notebooks-ca/notebooks-ca.crt",
"ca": ""
}
},
"services-ca": {
"serial": 0,
"is_ca": true,
"parent_ca": "",
"signees": null,
"files": {
"key": "/etc/jupyterhub/internal-ssl/services-ca/services-ca.key",
"cert": "/etc/jupyterhub/internal-ssl/services-ca/services-ca.crt",
"ca": ""
}
},
"proxy-api-ca": {
"serial": 0,
"is_ca": true,
"parent_ca": "",
"signees": {
"proxy-api": 1
},
"files": {
"key": "/etc/jupyterhub/internal-ssl/proxy-api-ca/proxy-api-ca.key",
"cert": "/etc/jupyterhub/internal-ssl/proxy-api-ca/proxy-api-ca.crt",
"ca": ""
}
},
"proxy-client-ca": {
"serial": 0,
"is_ca": true,
"parent_ca": "",
"signees": {
"proxy-client": 1
},
"files": {
"key": "/etc/jupyterhub/internal-ssl/proxy-client-ca/proxy-client-ca.key",
"cert": "/etc/jupyterhub/internal-ssl/proxy-client-ca/proxy-client-ca.crt",
"ca": ""
}
},
"hub-internal": {
"serial": 0,
"is_ca": false,
"parent_ca": "hub-ca",
"signees": null,
"files": {
"key": "/etc/jupyterhub/internal-ssl/hub-internal/hub-internal.key",
"cert": "/etc/jupyterhub/internal-ssl/hub-internal/hub-internal.crt",
"ca": "/etc/jupyterhub/internal-ssl/hub-ca/hub-ca.crt"
}
},
"proxy-api": {
"serial": 0,
"is_ca": false,
"parent_ca": "proxy-api-ca",
"signees": null,
"files": {
"key": "/etc/jupyterhub/internal-ssl/proxy-api/proxy-api.key",
"cert": "/etc/jupyterhub/internal-ssl/proxy-api/proxy-api.crt",
"ca": "/etc/jupyterhub/internal-ssl/proxy-api-ca/proxy-api-ca.crt"
}
},
"proxy-client": {
"serial": 0,
"is_ca": false,
"parent_ca": "proxy-client-ca",
"signees": null,
"files": {
"key": "/etc/jupyterhub/internal-ssl/proxy-client/proxy-client.key",
"cert": "/etc/jupyterhub/internal-ssl/proxy-client/proxy-client.crt",
"ca": "/etc/jupyterhub/internal-ssl/proxy-client-ca/proxy-client-ca.crt"
}
},
"user-service": {
"serial": 669,
"is_ca": false,
"parent_ca": "notebooks-ca",
"signees": null,
"files": {
"key": "/etc/jupyterhub/internal-ssl/user-service/user-service.key",
"cert": "/etc/jupyterhub/internal-ssl/user-service/user-service.crt",
"ca": "/etc/jupyterhub/internal-ssl/notebooks-ca/notebooks-ca.crt"
}
}
}
Now I am a little confused on how to make my custom proxy server utilize HTTPS and I think I am messing up on how I am creating the SSL context.
def create_ssl_context_for_server():
"""Create SSL context for this service to present its certificate"""
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(/etc/jupyterhub/internal-ssl/<my_website>_service.crt, /etc/jupyterhub/internal-ssl/<my_website>_service.key)
# Load the services CA to verify client certificates if needed
if os.path.exists(/etc/jupyterhub/internal-ssl/services-ca.crt):
context.load_verify_locations(/etc/jupyterhub/internal-ssl/services-ca.crt)
context.verify_mode = ssl.CERT_OPTIONAL
return context
The <my_website>_service.crt and <my_website>_service.key is generated with Opelssl from the services-ca, but I can’t reload Jupyter Hub as it’s throwing the error(s):
Jul 28 23:23:05 ip-172-16-0-86 python3[34817]: 2025-07-28 23:23:05,291 - WARNING - SSL Error on 8 ('127.0.0.1', 60000): [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)
Jul 28 23:23:05 ip-172-16-0-86 python3[34798]: [W 2025-07-28 23:23:05.292 JupyterHub iostream:779] error on read: [SSL: TLSV1_ALERT_UNKNOWN_CA] tlsv1 alert unknown ca (_ssl.c:2559)