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)