diff --git a/charmhelpers/contrib/hahelpers/cluster.py b/charmhelpers/contrib/hahelpers/cluster.py
index 47facd91cd6a9cbed27674d3025878a384ee1dcf..4a737e24e885cd767c7185eebd84a3afdf244b13 100644
--- a/charmhelpers/contrib/hahelpers/cluster.py
+++ b/charmhelpers/contrib/hahelpers/cluster.py
@@ -223,6 +223,11 @@ def https():
         return True
     if config_get('ssl_cert') and config_get('ssl_key'):
         return True
+    for r_id in relation_ids('certificates'):
+        for unit in relation_list(r_id):
+            ca = relation_get('ca', rid=r_id, unit=unit)
+            if ca:
+                return True
     for r_id in relation_ids('identity-service'):
         for unit in relation_list(r_id):
             # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
diff --git a/charmhelpers/contrib/openstack/amulet/utils.py b/charmhelpers/contrib/openstack/amulet/utils.py
index ef785423caf3557f5ff41c969ee8596778e40714..d43038b2f76f1ea5f576e06131a2298c5fd53837 100644
--- a/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/charmhelpers/contrib/openstack/amulet/utils.py
@@ -544,7 +544,7 @@ class OpenStackAmuletUtils(AmuletUtils):
         return ep
 
     def get_default_keystone_session(self, keystone_sentry,
-                                     openstack_release=None):
+                                     openstack_release=None, api_version=2):
         """Return a keystone session object and client object assuming standard
            default settings
 
@@ -559,12 +559,12 @@ class OpenStackAmuletUtils(AmuletUtils):
                eyc
         """
         self.log.debug('Authenticating keystone admin...')
-        api_version = 2
-        client_class = keystone_client.Client
         # 11 => xenial_queens
-        if openstack_release and openstack_release >= 11:
-            api_version = 3
+        if api_version == 3 or (openstack_release and openstack_release >= 11):
             client_class = keystone_client_v3.Client
+            api_version = 3
+        else:
+            client_class = keystone_client.Client
         keystone_ip = keystone_sentry.info['public-address']
         session, auth = self.get_keystone_session(
             keystone_ip,
diff --git a/charmhelpers/contrib/openstack/cert_utils.py b/charmhelpers/contrib/openstack/cert_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..de853b5371e915fb58bd6b29c1ea16a90ce7d08f
--- /dev/null
+++ b/charmhelpers/contrib/openstack/cert_utils.py
@@ -0,0 +1,227 @@
+# Copyright 2014-2018 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Common python helper functions used for OpenStack charm certificats.
+
+import os
+import json
+
+from charmhelpers.contrib.network.ip import (
+    get_hostname,
+    resolve_network_cidr,
+)
+from charmhelpers.core.hookenv import (
+    local_unit,
+    network_get_primary_address,
+    config,
+    relation_get,
+    unit_get,
+    NoNetworkBinding,
+    log,
+    WARNING,
+)
+from charmhelpers.contrib.openstack.ip import (
+    ADMIN,
+    resolve_address,
+    get_vip_in_network,
+    INTERNAL,
+    PUBLIC,
+    ADDRESS_MAP)
+
+from charmhelpers.core.host import (
+    mkdir,
+    write_file,
+)
+
+from charmhelpers.contrib.hahelpers.apache import (
+    install_ca_cert
+)
+
+
+class CertRequest(object):
+
+    """Create a request for certificates to be generated
+    """
+
+    def __init__(self, json_encode=True):
+        self.entries = []
+        self.hostname_entry = None
+        self.json_encode = json_encode
+
+    def add_entry(self, net_type, cn, addresses):
+        """Add a request to the batch
+
+        :param net_type: str netwrok space name request is for
+        :param cn: str Canonical Name for certificate
+        :param addresses: [] List of addresses to be used as SANs
+        """
+        self.entries.append({
+            'cn': cn,
+            'addresses': addresses})
+
+    def add_hostname_cn(self):
+        """Add a request for the hostname of the machine"""
+        ip = unit_get('private-address')
+        addresses = [ip]
+        # If a vip is being used without os-hostname config or
+        # network spaces then we need to ensure the local units
+        # cert has the approriate vip in the SAN list
+        vip = get_vip_in_network(resolve_network_cidr(ip))
+        if vip:
+            addresses.append(vip)
+        self.hostname_entry = {
+            'cn': get_hostname(ip),
+            'addresses': addresses}
+
+    def add_hostname_cn_ip(self, addresses):
+        """Add an address to the SAN list for the hostname request
+
+        :param addr: [] List of address to be added
+        """
+        for addr in addresses:
+            if addr not in self.hostname_entry['addresses']:
+                self.hostname_entry['addresses'].append(addr)
+
+    def get_request(self):
+        """Generate request from the batched up entries
+
+        """
+        if self.hostname_entry:
+            self.entries.append(self.hostname_entry)
+        request = {}
+        for entry in self.entries:
+            sans = sorted(list(set(entry['addresses'])))
+            request[entry['cn']] = {'sans': sans}
+        if self.json_encode:
+            return {'cert_requests': json.dumps(request, sort_keys=True)}
+        else:
+            return {'cert_requests': request}
+
+
+def get_certificate_request(json_encode=True):
+    """Generate a certificatee requests based on the network confioguration
+
+    """
+    req = CertRequest(json_encode=json_encode)
+    req.add_hostname_cn()
+    # Add os-hostname entries
+    for net_type in [INTERNAL, ADMIN, PUBLIC]:
+        net_config = config(ADDRESS_MAP[net_type]['override'])
+        try:
+            net_addr = resolve_address(endpoint_type=net_type)
+            ip = network_get_primary_address(
+                ADDRESS_MAP[net_type]['binding'])
+            addresses = [net_addr, ip]
+            vip = get_vip_in_network(resolve_network_cidr(ip))
+            if vip:
+                addresses.append(vip)
+            if net_config:
+                req.add_entry(
+                    net_type,
+                    net_config,
+                    addresses)
+            else:
+                # There is network address with no corresponding hostname.
+                # Add the ip to the hostname cert to allow for this.
+                req.add_hostname_cn_ip(addresses)
+        except NoNetworkBinding:
+            log("Skipping request for certificate for ip in {} space, no "
+                "local address found".format(net_type), WARNING)
+    return req.get_request()
+
+
+def create_ip_cert_links(ssl_dir, custom_hostname_link=None):
+    """Create symlinks for SAN records
+
+    :param ssl_dir: str Directory to create symlinks in
+    :param custom_hostname_link: str Additional link to be created
+    """
+    hostname = get_hostname(unit_get('private-address'))
+    hostname_cert = os.path.join(
+        ssl_dir,
+        'cert_{}'.format(hostname))
+    hostname_key = os.path.join(
+        ssl_dir,
+        'key_{}'.format(hostname))
+    # Add links to hostname cert, used if os-hostname vars not set
+    for net_type in [INTERNAL, ADMIN, PUBLIC]:
+        try:
+            addr = resolve_address(endpoint_type=net_type)
+            cert = os.path.join(ssl_dir, 'cert_{}'.format(addr))
+            key = os.path.join(ssl_dir, 'key_{}'.format(addr))
+            if os.path.isfile(hostname_cert) and not os.path.isfile(cert):
+                os.symlink(hostname_cert, cert)
+                os.symlink(hostname_key, key)
+        except NoNetworkBinding:
+            log("Skipping creating cert symlink for ip in {} space, no "
+                "local address found".format(net_type), WARNING)
+    if custom_hostname_link:
+        custom_cert = os.path.join(
+            ssl_dir,
+            'cert_{}'.format(custom_hostname_link))
+        custom_key = os.path.join(
+            ssl_dir,
+            'key_{}'.format(custom_hostname_link))
+        if os.path.isfile(hostname_cert) and not os.path.isfile(custom_cert):
+            os.symlink(hostname_cert, custom_cert)
+            os.symlink(hostname_key, custom_key)
+
+
+def install_certs(ssl_dir, certs, chain=None):
+    """Install the certs passed into the ssl dir and append the chain if
+       provided.
+
+    :param ssl_dir: str Directory to create symlinks in
+    :param certs: {} {'cn': {'cert': 'CERT', 'key': 'KEY'}}
+    :param chain: str Chain to be appended to certs
+    """
+    for cn, bundle in certs.items():
+        cert_filename = 'cert_{}'.format(cn)
+        key_filename = 'key_{}'.format(cn)
+        cert_data = bundle['cert']
+        if chain:
+            # Append chain file so that clients that trust the root CA will
+            # trust certs signed by an intermediate in the chain
+            cert_data = cert_data + chain
+        write_file(
+            path=os.path.join(ssl_dir, cert_filename),
+            content=cert_data, perms=0o640)
+        write_file(
+            path=os.path.join(ssl_dir, key_filename),
+            content=bundle['key'], perms=0o640)
+
+
+def process_certificates(service_name, relation_id, unit,
+                         custom_hostname_link=None):
+    """Process the certificates supplied down the relation
+
+    :param service_name: str Name of service the certifcates are for.
+    :param relation_id: str Relation id providing the certs
+    :param unit: str Unit providing the certs
+    :param custom_hostname_link: str Name of custom link to create
+    """
+    data = relation_get(rid=relation_id, unit=unit)
+    ssl_dir = os.path.join('/etc/apache2/ssl/', service_name)
+    mkdir(path=ssl_dir)
+    name = local_unit().replace('/', '_')
+    certs = data.get('{}.processed_requests'.format(name))
+    chain = data.get('chain')
+    ca = data.get('ca')
+    if certs:
+        certs = json.loads(certs)
+        install_ca_cert(ca.encode())
+        install_certs(ssl_dir, certs, chain)
+        create_ip_cert_links(
+            ssl_dir,
+            custom_hostname_link=custom_hostname_link)
diff --git a/charmhelpers/contrib/openstack/context.py b/charmhelpers/contrib/openstack/context.py
index 2d91f0a7759c190d729c8f079b669cb089b05797..b196d63fc7397e152f9e12bd8818892d76cc6bc2 100644
--- a/charmhelpers/contrib/openstack/context.py
+++ b/charmhelpers/contrib/openstack/context.py
@@ -789,17 +789,18 @@ class ApacheSSLContext(OSContextGenerator):
         ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
         mkdir(path=ssl_dir)
         cert, key = get_cert(cn)
-        if cn:
-            cert_filename = 'cert_{}'.format(cn)
-            key_filename = 'key_{}'.format(cn)
-        else:
-            cert_filename = 'cert'
-            key_filename = 'key'
+        if cert and key:
+            if cn:
+                cert_filename = 'cert_{}'.format(cn)
+                key_filename = 'key_{}'.format(cn)
+            else:
+                cert_filename = 'cert'
+                key_filename = 'key'
 
-        write_file(path=os.path.join(ssl_dir, cert_filename),
-                   content=b64decode(cert), perms=0o640)
-        write_file(path=os.path.join(ssl_dir, key_filename),
-                   content=b64decode(key), perms=0o640)
+            write_file(path=os.path.join(ssl_dir, cert_filename),
+                       content=b64decode(cert), perms=0o640)
+            write_file(path=os.path.join(ssl_dir, key_filename),
+                       content=b64decode(key), perms=0o640)
 
     def configure_ca(self):
         ca_cert = get_ca_cert()
@@ -871,23 +872,31 @@ class ApacheSSLContext(OSContextGenerator):
         if not self.external_ports or not https():
             return {}
 
-        self.configure_ca()
+        use_keystone_ca = True
+        for rid in relation_ids('certificates'):
+            if related_units(rid):
+                use_keystone_ca = False
+
+        if use_keystone_ca:
+            self.configure_ca()
+
         self.enable_modules()
 
         ctxt = {'namespace': self.service_namespace,
                 'endpoints': [],
                 'ext_ports': []}
 
-        cns = self.canonical_names()
-        if cns:
-            for cn in cns:
-                self.configure_cert(cn)
-        else:
-            # Expect cert/key provided in config (currently assumed that ca
-            # uses ip for cn)
-            for net_type in (INTERNAL, ADMIN, PUBLIC):
-                cn = resolve_address(endpoint_type=net_type)
-                self.configure_cert(cn)
+        if use_keystone_ca:
+            cns = self.canonical_names()
+            if cns:
+                for cn in cns:
+                    self.configure_cert(cn)
+            else:
+                # Expect cert/key provided in config (currently assumed that ca
+                # uses ip for cn)
+                for net_type in (INTERNAL, ADMIN, PUBLIC):
+                    cn = resolve_address(endpoint_type=net_type)
+                    self.configure_cert(cn)
 
         addresses = self.get_network_addresses()
         for address, endpoint in addresses:
diff --git a/charmhelpers/contrib/openstack/ip.py b/charmhelpers/contrib/openstack/ip.py
index d1476b1ab21d40934db6eb0cc0d2174d41b1df72..73102af7d5eec9fc0255acfeea211310b8d3794d 100644
--- a/charmhelpers/contrib/openstack/ip.py
+++ b/charmhelpers/contrib/openstack/ip.py
@@ -184,3 +184,13 @@ def resolve_address(endpoint_type=PUBLIC, override=True):
                          "clustered=%s)" % (net_type, clustered))
 
     return resolved_address
+
+
+def get_vip_in_network(network):
+    matching_vip = None
+    vips = config('vip')
+    if vips:
+        for vip in vips.split():
+            if is_address_in_network(network, vip):
+                matching_vip = vip
+    return matching_vip
diff --git a/hooks/certificates-relation-changed b/hooks/certificates-relation-changed
new file mode 120000
index 0000000000000000000000000000000000000000..dd3b3eff4b7109293b4cfd9b81f5fc49643432a0
--- /dev/null
+++ b/hooks/certificates-relation-changed
@@ -0,0 +1 @@
+keystone_hooks.py
\ No newline at end of file
diff --git a/hooks/certificates-relation-departed b/hooks/certificates-relation-departed
new file mode 120000
index 0000000000000000000000000000000000000000..dd3b3eff4b7109293b4cfd9b81f5fc49643432a0
--- /dev/null
+++ b/hooks/certificates-relation-departed
@@ -0,0 +1 @@
+keystone_hooks.py
\ No newline at end of file
diff --git a/hooks/certificates-relation-joined b/hooks/certificates-relation-joined
new file mode 120000
index 0000000000000000000000000000000000000000..dd3b3eff4b7109293b4cfd9b81f5fc49643432a0
--- /dev/null
+++ b/hooks/certificates-relation-joined
@@ -0,0 +1 @@
+keystone_hooks.py
\ No newline at end of file
diff --git a/hooks/keystone_hooks.py b/hooks/keystone_hooks.py
index d7407f606e60aba4fa0b47cd5904ff8c04db6a9e..6f8c9978b0621a240f0eff9dbe02fba73c602d09 100755
--- a/hooks/keystone_hooks.py
+++ b/hooks/keystone_hooks.py
@@ -148,6 +148,7 @@ from charmhelpers.contrib.openstack.ip import (
     ADMIN,
     resolve_address,
 )
+
 from charmhelpers.contrib.network.ip import (
     get_iface_for_address,
     get_netmask_for_address,
@@ -160,6 +161,11 @@ from charmhelpers.contrib.charmsupport import nrpe
 
 from charmhelpers.contrib.hardening.harden import harden
 
+from charmhelpers.contrib.openstack.cert_utils import (
+    get_certificate_request,
+    process_certificates,
+)
+
 hooks = Hooks()
 CONFIGS = register_configs()
 
@@ -952,6 +958,28 @@ def update_keystone_fid_service_provider(relation_id=None):
                  relation_settings=fid_settings)
 
 
+@hooks.hook('certificates-relation-joined')
+def certs_joined(relation_id=None):
+    relation_set(
+        relation_id=relation_id,
+        relation_settings=get_certificate_request())
+
+
+@hooks.hook('certificates-relation-changed')
+@restart_on_change(restart_map(), stopstart=True)
+def certs_changed(relation_id=None, unit=None):
+    # update_all_identity_relation_units calls the keystone API
+    # so configs need to be written and services restarted
+    # before
+    @restart_on_change(restart_map(), stopstart=True)
+    def write_certs_and_config():
+        process_certificates('keystone', relation_id, unit)
+        configure_https()
+    write_certs_and_config()
+    update_all_identity_relation_units()
+    update_all_domain_backends()
+
+
 def main():
     try:
         hooks.execute(sys.argv)
diff --git a/metadata.yaml b/metadata.yaml
index 2e80061d9a3f47ddbf4634bef3ffc8ccf6c76adf..49cf407537ce778797f9c0de2112b500b8609cc8 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -44,6 +44,8 @@ requires:
     scope: container
   websso-trusted-dashboard:
     interface: websso-trusted-dashboard
+  certificates:
+    interface: tls-certificates
 peers:
   cluster:
     interface: keystone-ha
diff --git a/tests/charmhelpers/contrib/openstack/amulet/utils.py b/tests/charmhelpers/contrib/openstack/amulet/utils.py
index ef785423caf3557f5ff41c969ee8596778e40714..d43038b2f76f1ea5f576e06131a2298c5fd53837 100644
--- a/tests/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/tests/charmhelpers/contrib/openstack/amulet/utils.py
@@ -544,7 +544,7 @@ class OpenStackAmuletUtils(AmuletUtils):
         return ep
 
     def get_default_keystone_session(self, keystone_sentry,
-                                     openstack_release=None):
+                                     openstack_release=None, api_version=2):
         """Return a keystone session object and client object assuming standard
            default settings
 
@@ -559,12 +559,12 @@ class OpenStackAmuletUtils(AmuletUtils):
                eyc
         """
         self.log.debug('Authenticating keystone admin...')
-        api_version = 2
-        client_class = keystone_client.Client
         # 11 => xenial_queens
-        if openstack_release and openstack_release >= 11:
-            api_version = 3
+        if api_version == 3 or (openstack_release and openstack_release >= 11):
             client_class = keystone_client_v3.Client
+            api_version = 3
+        else:
+            client_class = keystone_client.Client
         keystone_ip = keystone_sentry.info['public-address']
         session, auth = self.get_keystone_session(
             keystone_ip,
diff --git a/unit_tests/test_keystone_contexts.py b/unit_tests/test_keystone_contexts.py
index 42a7b7e5759dbd9e73635e839d21f7b20b506499..3368a9a87e01286ba8b60cb24e0e9f9ff9b511f2 100644
--- a/unit_tests/test_keystone_contexts.py
+++ b/unit_tests/test_keystone_contexts.py
@@ -104,8 +104,10 @@ class TestKeystoneContexts(CharmTestCase):
     @patch('charmhelpers.contrib.openstack.context.determine_apache_port')
     @patch('charmhelpers.contrib.openstack.context.determine_api_port')
     @patch('charmhelpers.contrib.openstack.context.unit_get')
+    @patch('charmhelpers.contrib.openstack.context.relation_ids')
     @patch('charmhelpers.contrib.openstack.context.https')
     def test_apache_ssl_context_service_enabled(self, mock_https,
+                                                mock_relation_ids,
                                                 mock_unit_get,
                                                 mock_determine_api_port,
                                                 mock_determine_apache_port,
@@ -118,6 +120,7 @@ class TestKeystoneContexts(CharmTestCase):
                                                 mock_ip_unit_get,
                                                 mock_rel_ids,
                                                 ):
+        mock_relation_ids.return_value = []
         mock_is_ssl_cert_master.return_value = True
         mock_https.return_value = True
         mock_unit_get.return_value = '1.2.3.4'