diff --git a/jupyterhub/app.py b/jupyterhub/app.py index acde4c4d..8d479c23 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1379,6 +1379,7 @@ class JupyterHub(Application): orm_service.admin = spec.get('admin', False) self.db.commit() service = Service(parent=self, + app=self, base_url=self.base_url, db=self.db, orm=orm_service, domain=domain, host=host, diff --git a/jupyterhub/proxy.py b/jupyterhub/proxy.py index fb80495e..23b87ec1 100644 --- a/jupyterhub/proxy.py +++ b/jupyterhub/proxy.py @@ -41,7 +41,7 @@ from jupyterhub.traitlets import Command from traitlets.config import LoggingConfigurable from .objects import Server from . import utils -from .utils import url_path_join +from .utils import url_path_join, make_ssl_context def _one_at_a_time(method): @@ -391,6 +391,15 @@ class ConfigurableHTTPProxy(Proxy): c.ConfigurableHTTPProxy.should_start = False """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + ssl_context = make_ssl_context( + self.app.internal_ssl_key, + self.app.internal_ssl_cert, + cafile=self.app.internal_ssl_ca, + ) + AsyncHTTPClient.configure(None, defaults={"ssl_options" : ssl_context}) + proxy_process = Any() client = Instance(AsyncHTTPClient, ()) @@ -432,9 +441,22 @@ class ConfigurableHTTPProxy(Proxy): token = utils.new_token() return token - api_url = Unicode('http://127.0.0.1:8001', config=True, + api_url = Unicode(config=True, help="""The ip (or hostname) of the proxy's API endpoint""" ) + + @default('api_url') + def _api_url_default(self): + url = '127.0.0.1:8001' + proto = 'http' + if self.app.internal_ssl: + proto = 'https' + + return "{proto}://{url}".format( + proto=proto, + url=url, + ) + command = Command('configurable-http-proxy', config=True, help="""The command to start the proxy""" ) @@ -541,6 +563,13 @@ class ConfigurableHTTPProxy(Proxy): cmd.extend(['--ssl-key', self.ssl_key]) 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]) + cmd.extend(['--api-ssl-request-cert']) + cmd.extend(['--api-ssl-reject-unauthorized']) + cmd.extend(['--forward-ssl']) if self.app.statsd_host: cmd.extend([ '--statsd-host', self.app.statsd_host, diff --git a/jupyterhub/services/auth.py b/jupyterhub/services/auth.py index 476ac936..2d97e1b5 100644 --- a/jupyterhub/services/auth.py +++ b/jupyterhub/services/auth.py @@ -196,6 +196,27 @@ class HubAuth(SingletonConfigurable): def _default_login_url(self): return self.hub_host + url_path_join(self.hub_prefix, 'login') + keyfile = Unicode('', + help="""The ssl key to use for requests + + Use with certfile + """ + ).tag(config=True) + + certfile = Unicode('', + help="""The ssl cert to use for requests + + Use with keyfile + """ + ).tag(config=True) + + client_ca = Unicode('', + help="""The ssl certificate authority to use to verify requests + + Use with keyfile and certfile + """ + ).tag(config=True) + cookie_name = Unicode('jupyterhub-services', help="""The name of the cookie I should be looking for""" ).tag(config=True) @@ -277,6 +298,10 @@ class HubAuth(SingletonConfigurable): allow_404 = kwargs.pop('allow_404', False) headers = kwargs.setdefault('headers', {}) headers.setdefault('Authorization', 'token %s' % self.api_token) + if "cert" not in kwargs and self.certfile and self.keyfile: + kwargs["cert"] = (self.certfile, self.keyfile) + if self.client_ca: + kwargs["verify"] = self.client_ca try: r = requests.request(method, url, **kwargs) except requests.ConnectionError as e: diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index 50bffa15..f4cab84d 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -224,6 +224,7 @@ class Service(LoggingConfigurable): domain = Unicode() host = Unicode() hub = Any() + app = Any() proc = Any() # handles on globals: @@ -331,6 +332,9 @@ class Service(LoggingConfigurable): server=self.orm.server, host=self.host, ), + internal_ssl=self.app.internal_ssl, + internal_certs_location=self.app.internal_certs_location, + internal_authority_name=self.app.internal_authority_name, ) self.spawner.start() self.proc = self.spawner.proc diff --git a/jupyterhub/singleuser.py b/jupyterhub/singleuser.py index 468c35da..ef043e01 100755 --- a/jupyterhub/singleuser.py +++ b/jupyterhub/singleuser.py @@ -237,6 +237,27 @@ class SingleUserNotebookApp(NotebookApp): def _default_group(self): return os.environ.get('JUPYTERHUB_GROUP') or '' + keyfile = Unicode('', + help="""The ssl key to use for requests + + Use with certfile + """ + ).tag(config=True) + + certfile = Unicode('', + help="""The ssl cert to use for requests + + Use with keyfile + """ + ).tag(config=True) + + client_ca = Unicode('', + help="""The ssl certificate authority to use to verify requests + + Use with keyfile and certfile + """ + ).tag(config=True) + @observe('user') def _user_changed(self, change): self.log.name = change.new @@ -423,6 +444,9 @@ class SingleUserNotebookApp(NotebookApp): api_url=self.hub_api_url, hub_prefix=self.hub_prefix, base_url=self.base_url, + keyfile=self.keyfile, + certfile=self.certfile, + client_ca=self.client_ca, ) # smoke check if not self.hub_auth.oauth_client_id: diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index ff76b66c..0079a35e 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -158,6 +158,11 @@ class Spawner(LoggingConfigurable): if self.orm_spawner: return self.orm_spawner.name return '' + hub = Any() + authenticator = Any() + internal_ssl = Bool(False) + internal_certs_location = Unicode('') + internal_authority_name = Unicode('') admin_access = Bool(False) api_token = Unicode() oauth_client_id = Unicode() diff --git a/jupyterhub/user.py b/jupyterhub/user.py index f65affc4..7a540727 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -11,7 +11,7 @@ from sqlalchemy import inspect from tornado import gen from tornado.log import app_log -from .utils import maybe_future, url_path_join +from .utils import maybe_future, url_path_join, make_ssl_context from . import orm from ._version import _check_version, __version__ @@ -215,6 +215,9 @@ class User: db=self.db, oauth_client_id=client_id, cookie_options = self.settings.get('cookie_options', {}), + 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'), ) # update with kwargs. Mainly for testing. spawn_kwargs.update(kwargs) @@ -493,7 +496,11 @@ class User: db.commit() spawner._waiting_for_response = True try: - resp = await server.wait_up(http=True, timeout=spawner.http_timeout) + key = self.settings['internal_ssl_key'] + cert = self.settings['internal_ssl_cert'] + ca = self.settings['internal_ssl_ca'] + ssl_context = make_ssl_context(key, cert, cafile=ca) + resp = await server.wait_up(http=True, timeout=spawner.http_timeout, ssl_context=ssl_context) except Exception as e: if isinstance(e, TimeoutError): self.log.warning(