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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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