mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-12 20:43:02 +00:00
Create and move certs for use with spawned notebooks
Add Localhost to trusted alt names Update to match refactored certipy names Add the FQDN to cert alt names for hub Ensure notebooks do not trust each other Drop certs in user's home directory Refactor cert creation and movement Make alt names configurable Make attaching alt names more generic Setup ssl_context for the singleuser hub check
This commit is contained in:
@@ -327,12 +327,20 @@ class JupyterHub(Application):
|
|||||||
Use with internal_ssl
|
Use with internal_ssl
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
internal_authority_name = Unicode('jupyterhub',
|
internal_authority_name = Unicode('hub',
|
||||||
help=""" The name for the internal signing authority
|
help=""" The name for the internal signing authority
|
||||||
|
|
||||||
Use with internal_ssl
|
Use with internal_ssl
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
internal_notebook_authority_name = Unicode('notebook',
|
||||||
|
help=""" The name for the notebook signing authority.
|
||||||
|
This authority is separate from internal_authority_name so that
|
||||||
|
individual notebooks do not trust each other, only the hub and proxy.
|
||||||
|
|
||||||
|
Use with internal_ssl
|
||||||
|
"""
|
||||||
|
).tag(config=True)
|
||||||
internal_ssl_key = Unicode('',
|
internal_ssl_key = Unicode('',
|
||||||
help=""" The key to be used for internal ssl
|
help=""" The key to be used for internal ssl
|
||||||
"""
|
"""
|
||||||
@@ -345,6 +353,16 @@ class JupyterHub(Application):
|
|||||||
help=""" The ca to be used for internal ssl
|
help=""" The ca to be used for internal ssl
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
trusted_alt_names = List(Unicode(),
|
||||||
|
help=""" Names to include in the subject alternative name.
|
||||||
|
These names will be used for server name verification. This is useful
|
||||||
|
if JupyterHub is being run behind a reverse proxy or services using ssl
|
||||||
|
are on different hosts.
|
||||||
|
|
||||||
|
Use with internal_ssl
|
||||||
|
"""
|
||||||
|
).tag(config=True)
|
||||||
|
|
||||||
ip = Unicode('',
|
ip = Unicode('',
|
||||||
help="""The public facing ip of the whole JupyterHub application
|
help="""The public facing ip of the whole JupyterHub application
|
||||||
(specifically referred to as the proxy).
|
(specifically referred to as the proxy).
|
||||||
@@ -1637,9 +1655,11 @@ class JupyterHub(Application):
|
|||||||
internal_ssl=self.internal_ssl,
|
internal_ssl=self.internal_ssl,
|
||||||
internal_certs_location=self.internal_certs_location,
|
internal_certs_location=self.internal_certs_location,
|
||||||
internal_authority_name=self.internal_authority_name,
|
internal_authority_name=self.internal_authority_name,
|
||||||
|
internal_notebook_authority_name=self.internal_notebook_authority_name,
|
||||||
internal_ssl_key=self.internal_ssl_key,
|
internal_ssl_key=self.internal_ssl_key,
|
||||||
internal_ssl_cert=self.internal_ssl_cert,
|
internal_ssl_cert=self.internal_ssl_cert,
|
||||||
internal_ssl_ca=self.internal_ssl_ca,
|
internal_ssl_ca=self.internal_ssl_ca,
|
||||||
|
trusted_alt_names=self.trusted_alt_names,
|
||||||
)
|
)
|
||||||
# allow configured settings to have priority
|
# allow configured settings to have priority
|
||||||
settings.update(self.tornado_settings)
|
settings.update(self.tornado_settings)
|
||||||
@@ -1685,17 +1705,37 @@ class JupyterHub(Application):
|
|||||||
self.update_config(cfg)
|
self.update_config(cfg)
|
||||||
if self.internal_ssl:
|
if self.internal_ssl:
|
||||||
cert_store = Certipy(store_dir=self.internal_certs_location)
|
cert_store = Certipy(store_dir=self.internal_certs_location)
|
||||||
cert_store.store_load()
|
joint_ca_file = "{out}/combined-cas.crt".format(out=self.internal_certs_location)
|
||||||
if not cert_store.store_get(self.internal_authority_name):
|
|
||||||
|
# The authority for internal components (hub, proxy)
|
||||||
|
if not cert_store.get(self.internal_authority_name):
|
||||||
cert_store.create_ca(self.internal_authority_name)
|
cert_store.create_ca(self.internal_authority_name)
|
||||||
internal_key_pair = cert_store.store_get("localhost")
|
|
||||||
|
# The authority for individual notebooks
|
||||||
|
notebook_authority = cert_store.get(self.internal_notebook_authority_name)
|
||||||
|
if not notebook_authority:
|
||||||
|
notebook_authority = cert_store.create_ca(self.internal_notebook_authority_name)
|
||||||
|
|
||||||
|
internal_key_pair = cert_store.get("localhost")
|
||||||
if not internal_key_pair:
|
if not internal_key_pair:
|
||||||
internal_key_pair = cert_store.create_signed_pair("localhost", self.internal_authority_name, alt_names=b"IP:127.0.0.1")
|
alt_names = "IP:127.0.0.1,DNS:localhost,{extra_names}"
|
||||||
cert_store.store_save()
|
# In the event the hub needs to be accessed externally, add
|
||||||
|
# the fqdn and (optionally) rev_proxy to the set of alt_names.
|
||||||
|
extra_names = [socket.getfqdn()] + self.trusted_alt_names
|
||||||
|
extra_names = ','.join(["DNS:{}".format(name) for name in extra_names])
|
||||||
|
alt_names = alt_names.format(extra_names=extra_names).encode()
|
||||||
|
internal_key_pair = cert_store.create_signed_pair("localhost", self.internal_authority_name, alt_names=alt_names)
|
||||||
|
|
||||||
|
# Join CA files
|
||||||
|
with open(internal_key_pair.ca_file) as internal_ca, \
|
||||||
|
open(notebook_authority.ca_file) as notebook_ca, \
|
||||||
|
open(joint_ca_file, 'w') as combined_ca:
|
||||||
|
combined_ca.write(internal_ca.read())
|
||||||
|
combined_ca.write(notebook_ca.read())
|
||||||
|
|
||||||
self.internal_ssl_key = internal_key_pair.key_file
|
self.internal_ssl_key = internal_key_pair.key_file
|
||||||
self.internal_ssl_cert = internal_key_pair.cert_file
|
self.internal_ssl_cert = internal_key_pair.cert_file
|
||||||
self.internal_ssl_ca = internal_key_pair.ca_file
|
self.internal_ssl_ca = joint_ca_file
|
||||||
self.write_pid_file()
|
self.write_pid_file()
|
||||||
|
|
||||||
def _log_cls(name, cls):
|
def _log_cls(name, cls):
|
||||||
|
@@ -43,7 +43,7 @@ from notebook.base.handlers import IPythonHandler
|
|||||||
from ._version import __version__, _check_version
|
from ._version import __version__, _check_version
|
||||||
from .log import log_request
|
from .log import log_request
|
||||||
from .services.auth import HubOAuth, HubOAuthenticated, HubOAuthCallbackHandler
|
from .services.auth import HubOAuth, HubOAuthenticated, HubOAuthCallbackHandler
|
||||||
from .utils import url_path_join
|
from .utils import url_path_join, make_ssl_context
|
||||||
|
|
||||||
|
|
||||||
# Authenticate requests with the Hub
|
# Authenticate requests with the Hub
|
||||||
@@ -399,6 +399,13 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
- exit if I can't connect at all
|
- exit if I can't connect at all
|
||||||
- check version and warn on sufficient mismatch
|
- check version and warn on sufficient mismatch
|
||||||
"""
|
"""
|
||||||
|
ssl_context = make_ssl_context(
|
||||||
|
self.keyfile,
|
||||||
|
self.certfile,
|
||||||
|
cafile=self.client_ca,
|
||||||
|
)
|
||||||
|
AsyncHTTPClient.configure(None, defaults={"ssl_options" : ssl_context})
|
||||||
|
|
||||||
client = AsyncHTTPClient()
|
client = AsyncHTTPClient()
|
||||||
RETRIES = 5
|
RETRIES = 5
|
||||||
for i in range(1, RETRIES+1):
|
for i in range(1, RETRIES+1):
|
||||||
|
@@ -164,6 +164,7 @@ class Spawner(LoggingConfigurable):
|
|||||||
internal_ssl = Bool(False)
|
internal_ssl = Bool(False)
|
||||||
internal_certs_location = Unicode('')
|
internal_certs_location = Unicode('')
|
||||||
internal_authority_name = Unicode('')
|
internal_authority_name = Unicode('')
|
||||||
|
internal_notebook_authority_name = Unicode('')
|
||||||
admin_access = Bool(False)
|
admin_access = Bool(False)
|
||||||
api_token = Unicode()
|
api_token = Unicode()
|
||||||
oauth_client_id = Unicode()
|
oauth_client_id = Unicode()
|
||||||
@@ -672,6 +673,47 @@ class Spawner(LoggingConfigurable):
|
|||||||
"""
|
"""
|
||||||
return s.format(**self.template_namespace())
|
return s.format(**self.template_namespace())
|
||||||
|
|
||||||
|
def create_certs(self):
|
||||||
|
"""Create the certs to be used for internal ssl."""
|
||||||
|
cert_store = Certipy(store_dir=self.internal_certs_location)
|
||||||
|
internal_authority = self.internal_authority_name
|
||||||
|
notebook_authority = self.internal_notebook_authority_name
|
||||||
|
internal_key_pair = cert_store.get(internal_authority)
|
||||||
|
notebook_key_pair = cert_store.create_signed_pair(self.user.name, notebook_authority, alt_names=b"DNS:localhost,IP:127.0.0.1")
|
||||||
|
return {
|
||||||
|
"key_file": notebook_key_pair.key_file,
|
||||||
|
"cert_file": notebook_key_pair.cert_file,
|
||||||
|
"ca_file": internal_key_pair.ca_file,
|
||||||
|
}
|
||||||
|
|
||||||
|
def move_certs(self, cert_files):
|
||||||
|
"""Takes dict of cert/ca file paths and moves, sets up proper ownership for them."""
|
||||||
|
user = pwd.getpwnam(self.user.name)
|
||||||
|
uid = user.pw_uid
|
||||||
|
gid = user.pw_gid
|
||||||
|
home = user.pw_dir
|
||||||
|
|
||||||
|
# Create dir for user's certs wherever we're starting
|
||||||
|
out_dir = "{home}/.jupyter".format(home=home)
|
||||||
|
shutil.rmtree(out_dir, ignore_errors=True)
|
||||||
|
os.makedirs(out_dir, 0o700, exist_ok=True)
|
||||||
|
|
||||||
|
# Move certs to users dir
|
||||||
|
shutil.move(cert_files['key_file'], out_dir)
|
||||||
|
shutil.move(cert_files['cert_file'], out_dir)
|
||||||
|
shutil.copy(cert_files['ca_file'], out_dir)
|
||||||
|
|
||||||
|
path_tmpl = "{out}/{name}.{ext}"
|
||||||
|
key = path_tmpl.format(out=out_dir, name=self.user.name, ext="key")
|
||||||
|
cert = path_tmpl.format(out=out_dir, name=self.user.name, ext="crt")
|
||||||
|
ca = path_tmpl.format(out=out_dir, name=self.internal_authority_name, ext="crt")
|
||||||
|
|
||||||
|
# Set cert ownership to user
|
||||||
|
for f in [out_dir, key, cert, ca]:
|
||||||
|
shutil.chown(f, user=uid, group=gid)
|
||||||
|
|
||||||
|
return [key, cert, ca]
|
||||||
|
|
||||||
def get_args(self):
|
def get_args(self):
|
||||||
"""Return the arguments to be passed after self.cmd
|
"""Return the arguments to be passed after self.cmd
|
||||||
|
|
||||||
@@ -696,21 +738,16 @@ class Spawner(LoggingConfigurable):
|
|||||||
args.append('--NotebookApp.default_url="%s"' % default_url)
|
args.append('--NotebookApp.default_url="%s"' % default_url)
|
||||||
|
|
||||||
if self.internal_ssl:
|
if self.internal_ssl:
|
||||||
cert_store = Certipy(store_dir=self.internal_certs_location)
|
try:
|
||||||
cert_store.store_load()
|
key, cert, ca = self.move_certs(self.create_certs())
|
||||||
authority = self.internal_authority_name
|
|
||||||
internal_key_pair = cert_store.store_get(self.user.name)
|
|
||||||
if not internal_key_pair:
|
|
||||||
internal_key_pair = cert_store.create_signed_pair(self.user.name, authority, alt_names=b"DNS:localhost,IP:127.0.0.1")
|
|
||||||
cert_store.store_save()
|
|
||||||
key = internal_key_pair.key_file
|
|
||||||
cert = internal_key_pair.cert_file
|
|
||||||
ca = internal_key_pair.ca_file
|
|
||||||
|
|
||||||
args.append('--keyfile="%s"' % key)
|
args.append('--keyfile="%s"' % key)
|
||||||
args.append('--certfile="%s"' % cert)
|
args.append('--certfile="%s"' % cert)
|
||||||
if ca:
|
if ca:
|
||||||
args.append('--client-ca="%s"' % ca)
|
args.append('--client-ca="%s"' % ca)
|
||||||
|
except Exception as e:
|
||||||
|
print("Internal SSL, if enabled, will not work.")
|
||||||
|
raise
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
args.append('--debug')
|
args.append('--debug')
|
||||||
|
@@ -218,6 +218,8 @@ class User:
|
|||||||
internal_ssl=self.settings.get('internal_ssl'),
|
internal_ssl=self.settings.get('internal_ssl'),
|
||||||
internal_certs_location=self.settings.get('internal_certs_location'),
|
internal_certs_location=self.settings.get('internal_certs_location'),
|
||||||
internal_authority_name=self.settings.get('internal_authority_name'),
|
internal_authority_name=self.settings.get('internal_authority_name'),
|
||||||
|
internal_notebook_authority_name=self.settings.get('internal_notebook_authority_name'),
|
||||||
|
trusted_alt_names=self.settings.get('trusted_alt_names'),
|
||||||
)
|
)
|
||||||
# update with kwargs. Mainly for testing.
|
# update with kwargs. Mainly for testing.
|
||||||
spawn_kwargs.update(kwargs)
|
spawn_kwargs.update(kwargs)
|
||||||
|
Reference in New Issue
Block a user