mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Use Certipy's trust graph to set up internal_ssl
With changes to CHP requiring a second, different authority, the complexity of managing trust within JupyterHub has risen. To solve this, Certipy now has a feature to specify what components should trust what and builds trust bundles accordingly.
This commit is contained in:
@@ -336,34 +336,54 @@ class JupyterHub(Application):
|
|||||||
Use with internal_ssl
|
Use with internal_ssl
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
internal_authority_name = Unicode('hub',
|
external_ssl_authorities = Dict(
|
||||||
help=""" The name for the internal signing authority
|
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
|
Use with internal_ssl
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
internal_notebook_authority_name = Unicode('notebook',
|
internal_ssl_authorities = Dict(
|
||||||
help=""" The name for the notebook signing authority.
|
default_value={
|
||||||
This authority is separate from internal_authority_name so that
|
'hub-ca': None,
|
||||||
individual notebooks do not trust each other, only the hub and proxy.
|
'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
|
Use with internal_ssl
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
)
|
||||||
external_authorities = Dict(
|
internal_ssl_components_trust = Dict(
|
||||||
help=""" A dict of common name to paths to external CA certificates.
|
help=""" Dict component:list(components). This dict specifies the
|
||||||
|
relationships of components secured by internal_ssl.
|
||||||
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
|
internal_trust_bundles = Dict(
|
||||||
it uses for SSL can be imported and its trust properly propagated.
|
help=""" Dict component:path. These are the paths to the trust bundles
|
||||||
|
that each component should have. They will be set during
|
||||||
The dict uses common names for keys that map to the file path of the
|
`init_internal_ssl`.
|
||||||
certificate. Common names need to be unique.
|
|
||||||
|
|
||||||
Use with internal_ssl
|
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
|
||||||
"""
|
"""
|
||||||
@@ -1138,26 +1158,37 @@ class JupyterHub(Application):
|
|||||||
self.cookie_secret = secret
|
self.cookie_secret = secret
|
||||||
|
|
||||||
def init_internal_ssl(self):
|
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:
|
if self.internal_ssl:
|
||||||
from certipy import Certipy, CertNotFoundError
|
from certipy import Certipy, CertNotFoundError
|
||||||
certipy = Certipy(store_dir=self.internal_certs_location,
|
certipy = Certipy(store_dir=self.internal_certs_location,
|
||||||
remove_existing=self.recreate_internal_certs)
|
remove_existing=self.recreate_internal_certs)
|
||||||
joint_ca_name = "combined-cas.crt"
|
|
||||||
|
|
||||||
# The authority for internal components (hub, proxy)
|
# Here we define how trust should be laid out per each component
|
||||||
try:
|
self.internal_ssl_components_trust = {
|
||||||
certipy.store.get_record(self.internal_authority_name)
|
'hub-ca': list(self.internal_ssl_authorities.keys()),
|
||||||
except CertNotFoundError:
|
'proxy-api-ca': ['hub-ca', 'services-ca', 'notebooks-ca'],
|
||||||
certipy.create_ca(self.internal_authority_name)
|
'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
|
hub_name = 'hub-ca'
|
||||||
try:
|
|
||||||
authority_record = certipy.store.get_record(
|
# If any external CAs were specified in external_ssl_authorities
|
||||||
self.internal_notebook_authority_name)
|
# add records of them to Certipy's store.
|
||||||
except CertNotFoundError:
|
self.internal_ssl_authorities.update(self.external_ssl_authorities)
|
||||||
authority_record = certipy.create_ca(
|
for authority, files in self.internal_ssl_authorities.items():
|
||||||
self.internal_notebook_authority_name)
|
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
|
# The signed certs used by hub-internal components
|
||||||
try:
|
try:
|
||||||
@@ -1171,30 +1202,13 @@ class JupyterHub(Application):
|
|||||||
+ self.trusted_alt_names)
|
+ self.trusted_alt_names)
|
||||||
internal_key_pair = certipy.create_signed_pair(
|
internal_key_pair = certipy.create_signed_pair(
|
||||||
"hub-internal",
|
"hub-internal",
|
||||||
self.internal_authority_name,
|
hub_name,
|
||||||
alt_names=alt_names
|
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_key = internal_key_pair['files']['key']
|
||||||
self.internal_ssl_cert = internal_key_pair['files']['cert']
|
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
|
# Configure the AsyncHTTPClient. This will affect anything using
|
||||||
# AsyncHTTPClient.
|
# AsyncHTTPClient.
|
||||||
@@ -1750,8 +1764,8 @@ class JupyterHub(Application):
|
|||||||
active_server_limit=self.active_server_limit,
|
active_server_limit=self.active_server_limit,
|
||||||
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_authorities=self.internal_ssl_authorities,
|
||||||
internal_notebook_authority_name=self.internal_notebook_authority_name,
|
internal_trust_bundles=self.internal_trust_bundles,
|
||||||
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,
|
||||||
|
@@ -121,6 +121,44 @@ class Proxy(LoggingConfigurable):
|
|||||||
if the proxy is to be started by the Hub
|
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):
|
def validate_routespec(self, routespec):
|
||||||
"""Validate a routespec
|
"""Validate a routespec
|
||||||
|
|
||||||
@@ -555,11 +593,27 @@ class ConfigurableHTTPProxy(Proxy):
|
|||||||
if self.ssl_cert:
|
if self.ssl_cert:
|
||||||
cmd.extend(['--ssl-cert', self.ssl_cert])
|
cmd.extend(['--ssl-cert', self.ssl_cert])
|
||||||
if self.app.internal_ssl:
|
if self.app.internal_ssl:
|
||||||
cmd.extend(['--api-ssl-key', self.app.internal_ssl_key])
|
certs = {}
|
||||||
cmd.extend(['--api-ssl-cert', self.app.internal_ssl_cert])
|
trust_bundles = {}
|
||||||
cmd.extend(['--api-ssl-ca', self.app.internal_ssl_ca])
|
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-request-cert'])
|
||||||
cmd.extend(['--api-ssl-reject-unauthorized'])
|
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:
|
if self.app.statsd_host:
|
||||||
cmd.extend([
|
cmd.extend([
|
||||||
'--statsd-host', self.app.statsd_host,
|
'--statsd-host', self.app.statsd_host,
|
||||||
|
@@ -334,7 +334,7 @@ class Service(LoggingConfigurable):
|
|||||||
),
|
),
|
||||||
internal_ssl=self.app.internal_ssl,
|
internal_ssl=self.app.internal_ssl,
|
||||||
internal_certs_location=self.app.internal_certs_location,
|
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.spawner.start()
|
||||||
self.proc = self.spawner.proc
|
self.proc = self.spawner.proc
|
||||||
|
@@ -162,9 +162,8 @@ class Spawner(LoggingConfigurable):
|
|||||||
hub = Any()
|
hub = Any()
|
||||||
authenticator = Any()
|
authenticator = Any()
|
||||||
internal_ssl = Bool(False)
|
internal_ssl = Bool(False)
|
||||||
|
internal_trust_bundles = Dict()
|
||||||
internal_certs_location = Unicode('')
|
internal_certs_location = 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()
|
||||||
@@ -716,19 +715,17 @@ class Spawner(LoggingConfigurable):
|
|||||||
alt_names = default_names + alt_names
|
alt_names = default_names + alt_names
|
||||||
|
|
||||||
certipy = Certipy(store_dir=self.internal_certs_location)
|
certipy = Certipy(store_dir=self.internal_certs_location)
|
||||||
internal_authority = self.internal_authority_name
|
notebook_component = 'notebooks-ca'
|
||||||
notebook_authority = self.internal_notebook_authority_name
|
|
||||||
internal_key_pair = certipy.store.get_record(internal_authority)
|
|
||||||
notebook_key_pair = certipy.create_signed_pair(
|
notebook_key_pair = certipy.create_signed_pair(
|
||||||
self.user.name,
|
self.user.name,
|
||||||
notebook_authority,
|
notebook_component,
|
||||||
alt_names=alt_names,
|
alt_names=alt_names,
|
||||||
overwrite=True
|
overwrite=True
|
||||||
)
|
)
|
||||||
paths = {
|
paths = {
|
||||||
"keyfile": notebook_key_pair['files']['key'],
|
"keyfile": notebook_key_pair['files']['key'],
|
||||||
"certfile": notebook_key_pair['files']['cert'],
|
"certfile": notebook_key_pair['files']['cert'],
|
||||||
"cafile": internal_key_pair['files']['cert'],
|
"cafile": self.internal_trust_bundles[notebook_component]
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -784,10 +781,9 @@ class Spawner(LoggingConfigurable):
|
|||||||
shutil.move(paths['certfile'], out_dir)
|
shutil.move(paths['certfile'], out_dir)
|
||||||
shutil.copy(paths['cafile'], out_dir)
|
shutil.copy(paths['cafile'], out_dir)
|
||||||
|
|
||||||
path_tmpl = "{out}/{name}.{ext}"
|
key = os.path.join(out_dir, os.path.basename(paths['keyfile']))
|
||||||
key = path_tmpl.format(out=out_dir, name=self.user.name, ext="key")
|
cert = os.path.join(out_dir, os.path.basename(paths['certfile']))
|
||||||
cert = path_tmpl.format(out=out_dir, name=self.user.name, ext="crt")
|
ca = os.path.join(out_dir, os.path.basename(paths['cafile']))
|
||||||
ca = path_tmpl.format(out=out_dir, name=self.internal_authority_name, ext="crt")
|
|
||||||
|
|
||||||
# Set cert ownership to user
|
# Set cert ownership to user
|
||||||
for f in [out_dir, key, cert, ca]:
|
for f in [out_dir, key, cert, ca]:
|
||||||
|
@@ -63,12 +63,10 @@ def app(request, io_loop, ssl_tmpdir):
|
|||||||
ssl_enabled = getattr(request.module, "ssl_enabled", False)
|
ssl_enabled = getattr(request.module, "ssl_enabled", False)
|
||||||
|
|
||||||
if ssl_enabled:
|
if ssl_enabled:
|
||||||
internal_authority_name = 'hub'
|
external_certs = ssl_setup(str(ssl_tmpdir), 'hub-ca')
|
||||||
external_certs = ssl_setup(str(ssl_tmpdir), internal_authority_name)
|
|
||||||
mocked_app = MockHub.instance(
|
mocked_app = MockHub.instance(
|
||||||
log_level=logging.DEBUG,
|
log_level=logging.DEBUG,
|
||||||
internal_ssl=True,
|
internal_ssl=True,
|
||||||
internal_authority_name=internal_authority_name,
|
|
||||||
internal_certs_location=str(ssl_tmpdir))
|
internal_certs_location=str(ssl_tmpdir))
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
|
@@ -219,11 +219,9 @@ class MockHub(JupyterHub):
|
|||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
# Turn on internalSSL if the options exist
|
# Turn on internalSSL if the options exist
|
||||||
internal_authority_name = 'hub'
|
|
||||||
cert_location = kwargs['internal_certs_location']
|
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_ssl'] = True
|
||||||
kwargs['internal_authority_name'] = internal_authority_name
|
|
||||||
kwargs['ssl_cert'] = external_certs['files']['cert']
|
kwargs['ssl_cert'] = external_certs['files']['cert']
|
||||||
kwargs['ssl_key'] = external_certs['files']['key']
|
kwargs['ssl_key'] = external_certs['files']['key']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@@ -221,9 +221,10 @@ class User:
|
|||||||
if self.settings.get('internal_ssl'):
|
if self.settings.get('internal_ssl'):
|
||||||
ssl_kwargs = dict(
|
ssl_kwargs = dict(
|
||||||
internal_ssl=self.settings.get('internal_ssl'),
|
internal_ssl=self.settings.get('internal_ssl'),
|
||||||
internal_certs_location=self.settings.get('internal_certs_location'),
|
internal_trust_bundles=self.settings.get(
|
||||||
internal_authority_name=self.settings.get('internal_authority_name'),
|
'internal_trust_bundles'),
|
||||||
internal_notebook_authority_name=self.settings.get('internal_notebook_authority_name'),
|
internal_certs_location=self.settings.get(
|
||||||
|
'internal_certs_location'),
|
||||||
)
|
)
|
||||||
spawn_kwargs.update(ssl_kwargs)
|
spawn_kwargs.update(ssl_kwargs)
|
||||||
|
|
||||||
@@ -505,9 +506,9 @@ class User:
|
|||||||
db.commit()
|
db.commit()
|
||||||
spawner._waiting_for_response = True
|
spawner._waiting_for_response = True
|
||||||
try:
|
try:
|
||||||
key = self.settings['internal_ssl_key']
|
key = self.settings.get('internal_ssl_key')
|
||||||
cert = self.settings['internal_ssl_cert']
|
cert = self.settings.get('internal_ssl_cert')
|
||||||
ca = self.settings['internal_ssl_ca']
|
ca = self.settings.get('internal_ssl_ca')
|
||||||
ssl_context = make_ssl_context(key, cert, cafile=ca)
|
ssl_context = make_ssl_context(key, cert, cafile=ca)
|
||||||
resp = await server.wait_up(
|
resp = await server.wait_up(
|
||||||
http=True,
|
http=True,
|
||||||
|
@@ -9,4 +9,4 @@ python-dateutil
|
|||||||
SQLAlchemy>=1.1
|
SQLAlchemy>=1.1
|
||||||
requests
|
requests
|
||||||
prometheus_client>=0.0.21
|
prometheus_client>=0.0.21
|
||||||
certipy>=0.1.0
|
certipy>=0.1.2
|
||||||
|
Reference in New Issue
Block a user