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:
Thomas Mendoza
2018-09-12 17:46:39 -07:00
parent ca33ccd66d
commit 67f19a65b7
8 changed files with 142 additions and 81 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,4 +9,4 @@ python-dateutil
SQLAlchemy>=1.1
requests
prometheus_client>=0.0.21
certipy>=0.1.0
certipy>=0.1.2