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:
Thomas Mendoza
2018-06-07 16:00:38 -07:00
parent c5faf2c5ea
commit 753bd0701f
4 changed files with 108 additions and 22 deletions

View File

@@ -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):

View File

@@ -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):

View File

@@ -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')

View File

@@ -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)