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
|
||||
"""
|
||||
).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,
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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]:
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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,
|
||||
|
@@ -9,4 +9,4 @@ python-dateutil
|
||||
SQLAlchemy>=1.1
|
||||
requests
|
||||
prometheus_client>=0.0.21
|
||||
certipy>=0.1.0
|
||||
certipy>=0.1.2
|
||||
|
Reference in New Issue
Block a user