From 753bd0701f98fc9e7865c843f5bbcc483ffa61dc Mon Sep 17 00:00:00 2001 From: Thomas Mendoza Date: Thu, 7 Jun 2018 16:00:38 -0700 Subject: [PATCH] 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 --- jupyterhub/app.py | 54 ++++++++++++++++++++++++++++----- jupyterhub/singleuser.py | 9 +++++- jupyterhub/spawner.py | 65 +++++++++++++++++++++++++++++++--------- jupyterhub/user.py | 2 ++ 4 files changed, 108 insertions(+), 22 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index aad98b87..c5338e87 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -327,12 +327,20 @@ class JupyterHub(Application): Use with internal_ssl """ ).tag(config=True) - internal_authority_name = Unicode('jupyterhub', + internal_authority_name = Unicode('hub', help=""" The name for the internal signing authority Use with internal_ssl """ ).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('', 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 """ ) + 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('', help="""The public facing ip of the whole JupyterHub application (specifically referred to as the proxy). @@ -1637,9 +1655,11 @@ class JupyterHub(Application): internal_ssl=self.internal_ssl, internal_certs_location=self.internal_certs_location, 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_cert=self.internal_ssl_cert, internal_ssl_ca=self.internal_ssl_ca, + trusted_alt_names=self.trusted_alt_names, ) # allow configured settings to have priority settings.update(self.tornado_settings) @@ -1685,17 +1705,37 @@ class JupyterHub(Application): self.update_config(cfg) if self.internal_ssl: cert_store = Certipy(store_dir=self.internal_certs_location) - cert_store.store_load() - if not cert_store.store_get(self.internal_authority_name): + joint_ca_file = "{out}/combined-cas.crt".format(out=self.internal_certs_location) + + # The authority for internal components (hub, proxy) + if not cert_store.get(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: - internal_key_pair = cert_store.create_signed_pair("localhost", self.internal_authority_name, alt_names=b"IP:127.0.0.1") - cert_store.store_save() + alt_names = "IP:127.0.0.1,DNS:localhost,{extra_names}" + # 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_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() def _log_cls(name, cls): diff --git a/jupyterhub/singleuser.py b/jupyterhub/singleuser.py index ef043e01..4ad27ad2 100755 --- a/jupyterhub/singleuser.py +++ b/jupyterhub/singleuser.py @@ -43,7 +43,7 @@ from notebook.base.handlers import IPythonHandler from ._version import __version__, _check_version from .log import log_request 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 @@ -399,6 +399,13 @@ class SingleUserNotebookApp(NotebookApp): - exit if I can't connect at all - 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() RETRIES = 5 for i in range(1, RETRIES+1): diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 51e4640a..19eb59c8 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -164,6 +164,7 @@ class Spawner(LoggingConfigurable): internal_ssl = Bool(False) internal_certs_location = Unicode('') internal_authority_name = Unicode('') + internal_notebook_authority_name = Unicode('') admin_access = Bool(False) api_token = Unicode() oauth_client_id = Unicode() @@ -672,6 +673,47 @@ class Spawner(LoggingConfigurable): """ 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): """Return the arguments to be passed after self.cmd @@ -696,21 +738,16 @@ class Spawner(LoggingConfigurable): args.append('--NotebookApp.default_url="%s"' % default_url) if self.internal_ssl: - cert_store = Certipy(store_dir=self.internal_certs_location) - cert_store.store_load() - 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 + try: + key, cert, ca = self.move_certs(self.create_certs()) - args.append('--keyfile="%s"' % key) - args.append('--certfile="%s"' % cert) - if ca: - args.append('--client-ca="%s"' % ca) + args.append('--keyfile="%s"' % key) + args.append('--certfile="%s"' % cert) + if ca: + args.append('--client-ca="%s"' % ca) + except Exception as e: + print("Internal SSL, if enabled, will not work.") + raise if self.debug: args.append('--debug') diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 7a540727..282b5445 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -218,6 +218,8 @@ class User: internal_ssl=self.settings.get('internal_ssl'), internal_certs_location=self.settings.get('internal_certs_location'), 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. spawn_kwargs.update(kwargs)