diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 32c0e50b..e2a237d2 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -336,34 +336,54 @@ class JupyterHub(Application): Use with internal_ssl """ ).tag(config=True) - internal_authority_name = Unicode('hub', - help=""" The name for the internal signing authority + external_ssl_authorities = Dict( + default_value={}, + help=""" Dict authority:dict(files). Specify the key, cert, and/or + ca file for an authority. This is useful for externally managed + proxies that wish to use internal_ssl. + + The files dict has this format (you must specify at least a cert): + + { + 'key': '/path/to/key.key', + 'cert': '/path/to/cert.crt', + 'ca': '/path/to/ca.crt' + } + + The authorities you can override: 'hub-ca', 'notebooks-ca', + 'proxy-api-ca', 'proxy-client-ca', and 'services-ca'. 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. + internal_ssl_authorities = Dict( + default_value={ + 'hub-ca': None, + 'notebooks-ca': None, + 'proxy-api-ca': None, + 'proxy-client-ca': None, + 'services-ca': None, + }, + help=""" Dict authority:dict(files). When creating the various + CAs needed for internal_ssl, these are the names that will be used + for each authority. Use with internal_ssl """ - ).tag(config=True) - external_authorities = Dict( - help=""" A dict of common name to paths to external CA certificates. - - This option is useful when there exist certificates that need to be - trusted, but are outside of Certipy's control. For example, when - running a proxy in lieu of ConfigurableHTTPProxy, the signing CA that - it uses for SSL can be imported and its trust properly propagated. - - The dict uses common names for keys that map to the file path of the - certificate. Common names need to be unique. + ) + internal_ssl_components_trust = Dict( + help=""" Dict component:list(components). This dict specifies the + relationships of components secured by internal_ssl. + """ + ) + internal_trust_bundles = Dict( + help=""" Dict component:path. These are the paths to the trust bundles + that each component should have. They will be set during + `init_internal_ssl`. Use with internal_ssl """ - ).tag(config=True) + ) internal_ssl_key = Unicode('', help=""" The key to be used for internal ssl """ @@ -1138,26 +1158,37 @@ class JupyterHub(Application): self.cookie_secret = secret def init_internal_ssl(self): - """Create the certs needed to turn on internal SSL""" + """Create the certs needed to turn on internal SSL. + + + """ + if self.internal_ssl: from certipy import Certipy, CertNotFoundError certipy = Certipy(store_dir=self.internal_certs_location, remove_existing=self.recreate_internal_certs) - joint_ca_name = "combined-cas.crt" - # The authority for internal components (hub, proxy) - try: - certipy.store.get_record(self.internal_authority_name) - except CertNotFoundError: - certipy.create_ca(self.internal_authority_name) + # Here we define how trust should be laid out per each component + self.internal_ssl_components_trust = { + 'hub-ca': list(self.internal_ssl_authorities.keys()), + 'proxy-api-ca': ['hub-ca', 'services-ca', 'notebooks-ca'], + 'proxy-client-ca': ['hub-ca', 'notebooks-ca'], + 'notebooks-ca': ['hub-ca', 'proxy-client-ca'], + 'services-ca': ['hub-ca', 'proxy-api-ca'], + } - # The authority for individual notebooks - try: - authority_record = certipy.store.get_record( - self.internal_notebook_authority_name) - except CertNotFoundError: - authority_record = certipy.create_ca( - self.internal_notebook_authority_name) + hub_name = 'hub-ca' + + # If any external CAs were specified in external_ssl_authorities + # add records of them to Certipy's store. + self.internal_ssl_authorities.update(self.external_ssl_authorities) + for authority, files in self.internal_ssl_authorities.items(): + if files: + certipy.store.add_record( + authority, is_ca=True, files=files) + + self.internal_trust_bundles = certipy.trust_from_graph( + self.internal_ssl_components_trust) # The signed certs used by hub-internal components try: @@ -1171,30 +1202,13 @@ class JupyterHub(Application): + self.trusted_alt_names) internal_key_pair = certipy.create_signed_pair( "hub-internal", - self.internal_authority_name, + hub_name, alt_names=alt_names ) - external_authorities = [] - # Add provided, external authority info into Certipy. This will - # allow these certs to be added to the joint trust bundle. - for cn, file_path in self.external_authorities.items(): - certipy.store.add_record( - cn, files={"cert": file_path} - ) - external_authorities.append(cn) - - authorities = ([self.internal_authority_name, - self.internal_notebook_authority_name] - + external_authorities) - - joint_ca_file = certipy.create_ca_bundle( - joint_ca_name, ca_names=authorities - ) - self.internal_ssl_key = internal_key_pair['files']['key'] self.internal_ssl_cert = internal_key_pair['files']['cert'] - self.internal_ssl_ca = joint_ca_file + self.internal_ssl_ca = self.internal_trust_bundles[hub_name] # Configure the AsyncHTTPClient. This will affect anything using # AsyncHTTPClient. @@ -1750,8 +1764,8 @@ class JupyterHub(Application): active_server_limit=self.active_server_limit, 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_authorities=self.internal_ssl_authorities, + internal_trust_bundles=self.internal_trust_bundles, internal_ssl_key=self.internal_ssl_key, internal_ssl_cert=self.internal_ssl_cert, internal_ssl_ca=self.internal_ssl_ca, diff --git a/jupyterhub/proxy.py b/jupyterhub/proxy.py index 9b6a263f..03a824ec 100644 --- a/jupyterhub/proxy.py +++ b/jupyterhub/proxy.py @@ -121,6 +121,44 @@ class Proxy(LoggingConfigurable): if the proxy is to be started by the Hub """ + def create_certs(self, name, ca_name, alt_names=None, override=False): + """Create certificates to be used with internal_ssl. + """ + + from certipy import Certipy, CertNotFoundError + default_names = ["DNS:localhost", "IP:127.0.0.1"] + alt_names = alt_names or [] + + if not override: + alt_names = default_names + alt_names + + certipy = Certipy(store_dir=self.app.internal_certs_location) + record = None + + try: + record = certipy.store.get_record(name) + except CertNotFoundError: + record = certipy.create_signed_pair( + name, + ca_name, + alt_names=alt_names, + overwrite=True + ) + + paths = { + "keyfile": record['files']['key'], + "certfile": record['files']['cert'], + "cafile": record['files']['cert'], + } + + return paths + + + def move_certs(self, paths): + """Move certificates created by create_certs. + """ + return paths + def validate_routespec(self, routespec): """Validate a routespec @@ -555,11 +593,27 @@ class ConfigurableHTTPProxy(Proxy): if self.ssl_cert: cmd.extend(['--ssl-cert', self.ssl_cert]) if self.app.internal_ssl: - cmd.extend(['--api-ssl-key', self.app.internal_ssl_key]) - cmd.extend(['--api-ssl-cert', self.app.internal_ssl_cert]) - cmd.extend(['--api-ssl-ca', self.app.internal_ssl_ca]) + certs = {} + trust_bundles = {} + proxy_api = 'proxy-api' + proxy_client = 'proxy-client' + for component in [proxy_api, proxy_client]: + ca_name = component + '-ca' + trust_bundles[component] = \ + self.app.internal_trust_bundles[ca_name] + certs[component] = self.move_certs( + self.create_certs(component, ca_name)) + cmd.extend(['--api-ssl-key', certs[proxy_api]['keyfile']]) + cmd.extend(['--api-ssl-cert', certs[proxy_api]['certfile']]) + cmd.extend(['--api-ssl-ca', trust_bundles[proxy_api]]) cmd.extend(['--api-ssl-request-cert']) cmd.extend(['--api-ssl-reject-unauthorized']) + + cmd.extend(['--client-ssl-key', certs[proxy_client]['keyfile']]) + cmd.extend(['--client-ssl-cert', certs[proxy_client]['certfile']]) + cmd.extend(['--client-ssl-ca', trust_bundles[proxy_client]]) + cmd.extend(['--client-ssl-request-cert']) + cmd.extend(['--client-ssl-reject-unauthorized']) if self.app.statsd_host: cmd.extend([ '--statsd-host', self.app.statsd_host, diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index f4cab84d..9ddd4b45 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -334,7 +334,7 @@ class Service(LoggingConfigurable): ), internal_ssl=self.app.internal_ssl, internal_certs_location=self.app.internal_certs_location, - internal_authority_name=self.app.internal_authority_name, + internal_trust_bundles=self.app.internal_trust_bundles, ) self.spawner.start() self.proc = self.spawner.proc diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 25d5b93b..1116cb8c 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -162,9 +162,8 @@ class Spawner(LoggingConfigurable): hub = Any() authenticator = Any() internal_ssl = Bool(False) + internal_trust_bundles = Dict() internal_certs_location = Unicode('') - internal_authority_name = Unicode('') - internal_notebook_authority_name = Unicode('') admin_access = Bool(False) api_token = Unicode() oauth_client_id = Unicode() @@ -716,19 +715,17 @@ class Spawner(LoggingConfigurable): alt_names = default_names + alt_names certipy = Certipy(store_dir=self.internal_certs_location) - internal_authority = self.internal_authority_name - notebook_authority = self.internal_notebook_authority_name - internal_key_pair = certipy.store.get_record(internal_authority) + notebook_component = 'notebooks-ca' notebook_key_pair = certipy.create_signed_pair( self.user.name, - notebook_authority, + notebook_component, alt_names=alt_names, overwrite=True ) paths = { "keyfile": notebook_key_pair['files']['key'], "certfile": notebook_key_pair['files']['cert'], - "cafile": internal_key_pair['files']['cert'], + "cafile": self.internal_trust_bundles[notebook_component] } try: @@ -784,10 +781,9 @@ class Spawner(LoggingConfigurable): shutil.move(paths['certfile'], out_dir) shutil.copy(paths['cafile'], 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") + key = os.path.join(out_dir, os.path.basename(paths['keyfile'])) + cert = os.path.join(out_dir, os.path.basename(paths['certfile'])) + ca = os.path.join(out_dir, os.path.basename(paths['cafile'])) # Set cert ownership to user for f in [out_dir, key, cert, ca]: diff --git a/jupyterhub/tests/conftest.py b/jupyterhub/tests/conftest.py index 9e11d7c0..2f2e7009 100644 --- a/jupyterhub/tests/conftest.py +++ b/jupyterhub/tests/conftest.py @@ -63,12 +63,10 @@ def app(request, io_loop, ssl_tmpdir): ssl_enabled = getattr(request.module, "ssl_enabled", False) if ssl_enabled: - internal_authority_name = 'hub' - external_certs = ssl_setup(str(ssl_tmpdir), internal_authority_name) + external_certs = ssl_setup(str(ssl_tmpdir), 'hub-ca') mocked_app = MockHub.instance( log_level=logging.DEBUG, internal_ssl=True, - internal_authority_name=internal_authority_name, internal_certs_location=str(ssl_tmpdir)) @gen.coroutine diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index db3f6472..f82e51c8 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -219,11 +219,9 @@ class MockHub(JupyterHub): def __new__(cls, *args, **kwargs): try: # Turn on internalSSL if the options exist - internal_authority_name = 'hub' cert_location = kwargs['internal_certs_location'] - external_certs = ssl_setup(cert_location, internal_authority_name) + external_certs = ssl_setup(cert_location, 'hub-ca') kwargs['internal_ssl'] = True - kwargs['internal_authority_name'] = internal_authority_name kwargs['ssl_cert'] = external_certs['files']['cert'] kwargs['ssl_key'] = external_certs['files']['key'] except KeyError: diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 6f42a983..2013d74d 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -221,9 +221,10 @@ class User: if self.settings.get('internal_ssl'): ssl_kwargs = dict( 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'), + internal_trust_bundles=self.settings.get( + 'internal_trust_bundles'), + internal_certs_location=self.settings.get( + 'internal_certs_location'), ) spawn_kwargs.update(ssl_kwargs) @@ -505,9 +506,9 @@ class User: db.commit() spawner._waiting_for_response = True try: - key = self.settings['internal_ssl_key'] - cert = self.settings['internal_ssl_cert'] - ca = self.settings['internal_ssl_ca'] + key = self.settings.get('internal_ssl_key') + cert = self.settings.get('internal_ssl_cert') + ca = self.settings.get('internal_ssl_ca') ssl_context = make_ssl_context(key, cert, cafile=ca) resp = await server.wait_up( http=True, diff --git a/requirements.txt b/requirements.txt index 92eb07ac..9b7688a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ python-dateutil SQLAlchemy>=1.1 requests prometheus_client>=0.0.21 -certipy>=0.1.0 +certipy>=0.1.2