feat(cert-manager): migrate to operator + add docs
diff --git a/atmosphere/cmd/operator.py b/atmosphere/cmd/operator.py
index c8dad4b..78cfc85 100644
--- a/atmosphere/cmd/operator.py
+++ b/atmosphere/cmd/operator.py
@@ -9,7 +9,7 @@
 def main():
     LOG.info("Starting Atmosphere operator")
 
-    cfg = config.Config.load_from_file()
+    cfg = config.Config.from_file()
 
     engine = flows.get_engine(cfg)
     engine.run()
diff --git a/atmosphere/flows.py b/atmosphere/flows.py
index 43c254c..55d490b 100644
--- a/atmosphere/flows.py
+++ b/atmosphere/flows.py
@@ -3,7 +3,7 @@
 
 from atmosphere.tasks import constants
 from atmosphere.tasks.composite import openstack_helm
-from atmosphere.tasks.kubernetes import flux, v1
+from atmosphere.tasks.kubernetes import cert_manager, flux, v1
 
 
 def get_engine(config):
@@ -88,9 +88,7 @@
             version=constants.HELM_RELEASE_PXC_OPERATOR_VERSION,
             values=constants.HELM_RELEASE_PXC_OPERATOR_VALUES,
         ),
-        openstack_helm.ApplyPerconaXtraDBClusterTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-        ),
+        openstack_helm.ApplyPerconaXtraDBClusterTask(),
         flux.ApplyHelmRepositoryTask(
             namespace=constants.NAMESPACE_OPENSTACK,
             name=constants.HELM_REPOSITORY_INGRESS_NGINX,
@@ -145,6 +143,9 @@
         ),
     )
 
+    for t in cert_manager.ApplyIssuerTask.from_config(config.issuer):
+        flow.add(t)
+
     if config.memcached.enabled:
         flow.add(
             openstack_helm.ApplyReleaseSecretTask(
diff --git a/atmosphere/models/config.py b/atmosphere/models/config.py
index 2e4c3bb..f5a7fe9 100644
--- a/atmosphere/models/config.py
+++ b/atmosphere/models/config.py
@@ -9,7 +9,9 @@
 
 
 class AcmeIssuerSolverConfig(base.Model):
-    type = types.StringType(choices=("http", "route53"), default="http", required=True)
+    type = types.StringType(
+        choices=("http", "rfc2136", "route53"), default="http", required=True
+    )
 
     @classmethod
     def _claim_polymorphic(cls, data):
@@ -20,17 +22,27 @@
     TYPE = "http"
 
 
+class Rfc2136AcmeIssuerSolverConfig(AcmeIssuerSolverConfig):
+    TYPE = "rfc2136"
+
+    nameserver = types.StringType(required=True)
+    tsig_algorithm = types.StringType(required=True)
+    tsig_key_name = types.StringType(required=True)
+    tsig_secret = types.StringType(required=True)
+
+
 class Route53AcmeIssuerSolverConfig(AcmeIssuerSolverConfig):
     TYPE = "route53"
 
+    region = types.StringType(default="global", required=True)
+    hosted_zone_id = types.StringType(required=True)
     access_key_id = types.StringType(required=True)
     secret_access_key = types.StringType(required=True)
-    hosted_zone_id = types.StringType(required=True)
 
 
 class Issuer(base.Model):
     type = types.StringType(
-        choices=("self-signed", "acme"), default="acme", required=True
+        choices=("acme", "ca", "self-signed"), default="acme", required=True
     )
 
     @classmethod
@@ -44,10 +56,23 @@
     email = types.StringType(required=True)
     server = types.URLType(default="https://acme-v02.api.letsencrypt.org/directory")
     solver = types.PolyModelType(
-        [HttpAcmeIssuerSolverConfig, Route53AcmeIssuerSolverConfig], required=True
+        [
+            HttpAcmeIssuerSolverConfig,
+            Rfc2136AcmeIssuerSolverConfig,
+            Route53AcmeIssuerSolverConfig,
+        ],
+        default=HttpAcmeIssuerSolverConfig(),
+        required=True,
     )
 
 
+class CaIssuerConfig(Issuer):
+    TYPE = "ca"
+
+    certificate = types.StringType(required=True)
+    private_key = types.StringType(required=True)
+
+
 class SelfSignedIssuerConfig(Issuer):
     TYPE = "self-signed"
 
@@ -68,12 +93,26 @@
     memcached = types.ModelType(
         MemcachedConfig, default=MemcachedConfig(), required=True
     )
-    issuer = types.DictType(
-        types.PolyModelType([AcmeIssuerConfig, SelfSignedIssuerConfig])
+    issuer = types.PolyModelType(
+        [AcmeIssuerConfig, CaIssuerConfig, SelfSignedIssuerConfig],
+        default=AcmeIssuerConfig(),
+        required=True,
     )
 
     @classmethod
-    def load_from_file(cls, path=CONFIG_FILE):
+    def from_toml(cls, data, validate=True):
+        c = cls(data, validate=validate)
+        if validate:
+            c.validate()
+        return c
+
+    @classmethod
+    def from_file(cls, path=CONFIG_FILE):
         with open(path, "rb") as fd:
             data = tomli.load(fd)
-            return cls(data, validate=True)
+            return cls.from_toml(data)
+
+    @classmethod
+    def from_string(cls, data: str, validate=True):
+        data = tomli.loads(data)
+        return cls.from_toml(data, validate)
diff --git a/atmosphere/tasks/composite/openstack_helm.py b/atmosphere/tasks/composite/openstack_helm.py
index 7f189b1..22d221d 100644
--- a/atmosphere/tasks/composite/openstack_helm.py
+++ b/atmosphere/tasks/composite/openstack_helm.py
@@ -10,9 +10,7 @@
 
 
 class ApplyReleaseSecretTask(v1.ApplySecretTask):
-    def __init__(
-        self, config: config.Config, namespace: str, chart: str, *args, **kwargs
-    ):
+    def __init__(self, config: config.Config, namespace: str, chart: str):
         vals = mergedeep.merge(
             {},
             values.Values.for_chart(chart, config).to_native(),
@@ -21,11 +19,9 @@
         values_yaml = yaml.dump(vals, default_flow_style=False)
 
         super().__init__(
-            namespace,
-            f"atmosphere-{chart}",
-            {"values.yaml": base64.encode_as_text(values_yaml)},
-            *args,
-            **kwargs,
+            namespace=namespace,
+            name=f"atmosphere-{chart}",
+            data={"values.yaml": base64.encode_as_text(values_yaml)},
         )
 
 
@@ -60,27 +56,25 @@
 
 
 class ApplyPerconaXtraDBClusterTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, namespace: str):
+    def __init__(self):
         super().__init__(
             kind=PerconaXtraDBCluster,
-            namespace=namespace,
+            namespace=constants.NAMESPACE_OPENSTACK,
             name="percona-xtradb",
             requires=[
-                f"helm-release-{namespace}-{constants.HELM_RELEASE_PXC_OPERATOR_NAME}",
-                "name",
+                f"helm-release-{constants.NAMESPACE_OPENSTACK}-{constants.HELM_RELEASE_PXC_OPERATOR_NAME}",
             ],
-            inject={"name": "percona-xtradb"},
         )
 
-    def generate_object(self, namespace, name, **kwargs) -> PerconaXtraDBCluster:
+    def generate_object(self) -> PerconaXtraDBCluster:
         return PerconaXtraDBCluster(
             self.api,
             {
                 "apiVersion": self._obj_kind.version,
                 "kind": self._obj_kind.kind,
                 "metadata": {
-                    "name": name,
-                    "namespace": namespace.name,
+                    "name": self._obj_name,
+                    "namespace": self._obj_namespace,
                 },
                 "spec": {
                     "crVersion": "1.10.0",
@@ -147,20 +141,18 @@
             name=name,
             requires=[
                 f"helm-release-{constants.NAMESPACE_OPENSTACK}-{constants.HELM_RELEASE_RABBITMQ_OPERATOR_NAME}",
-                "name",
             ],
-            inject={"name": name},
         )
 
-    def generate_object(self, namespace, name, **kwargs) -> RabbitmqCluster:
+    def generate_object(self) -> RabbitmqCluster:
         return RabbitmqCluster(
             self.api,
             {
                 "apiVersion": self._obj_kind.version,
                 "kind": self._obj_kind.kind,
                 "metadata": {
-                    "name": f"rabbitmq-{name}",
-                    "namespace": namespace.name,
+                    "name": f"rabbitmq-{self._obj_name}",
+                    "namespace": self._obj_namespace,
                 },
                 "spec": {
                     "affinity": {
diff --git a/atmosphere/tasks/kubernetes/base.py b/atmosphere/tasks/kubernetes/base.py
index c675307..7159310 100644
--- a/atmosphere/tasks/kubernetes/base.py
+++ b/atmosphere/tasks/kubernetes/base.py
@@ -59,7 +59,7 @@
     def execute(self, *args, **kwargs):
         self.logger.debug("Ensuring resource")
 
-        resource = self.generate_object(*args, **kwargs)
+        resource = self.generate_object()
         resp = resource.api.patch(
             **resource.api_kwargs(
                 headers={
diff --git a/atmosphere/tasks/kubernetes/cert_manager.py b/atmosphere/tasks/kubernetes/cert_manager.py
new file mode 100644
index 0000000..6d213a9
--- /dev/null
+++ b/atmosphere/tasks/kubernetes/cert_manager.py
@@ -0,0 +1,220 @@
+import pykube
+from oslo_serialization import base64
+
+from atmosphere.models import config
+from atmosphere.tasks import constants
+from atmosphere.tasks.kubernetes import base, v1
+
+
+class Certificate(pykube.objects.NamespacedAPIObject):
+    version = "cert-manager.io/v1"
+    endpoint = "certificates"
+    kind = "Certificate"
+
+
+class ApplyCertificateTask(base.ApplyKubernetesObjectTask):
+    def __init__(self, namespace: str, name: str, spec: dict):
+        self._spec = spec
+
+        super().__init__(
+            kind=Certificate,
+            namespace=namespace,
+            name=name,
+            requires=set(["namespace"]),
+        )
+
+    def generate_object(self) -> Certificate:
+        return Certificate(
+            self.api,
+            {
+                "apiVersion": self._obj_kind.version,
+                "kind": self._obj_kind.kind,
+                "metadata": {
+                    "name": self._obj_name,
+                    "namespace": self._obj_namespace,
+                },
+                "spec": self._spec,
+            },
+        )
+
+
+class Issuer(pykube.objects.NamespacedAPIObject):
+    version = "cert-manager.io/v1"
+    endpoint = "issuers"
+    kind = "Issuer"
+
+
+class ApplyIssuerTask(base.ApplyKubernetesObjectTask):
+    def __init__(self, namespace: str, name: str, spec: dict):
+        self._spec = spec
+
+        super().__init__(
+            kind=Issuer,
+            namespace=namespace,
+            name=name,
+            requires=set(
+                [
+                    f"helm-release-{constants.NAMESPACE_CERT_MANAGER}-{constants.HELM_RELEASE_CERT_MANAGER_NAME}",
+                ]
+            ),
+        )
+
+    def generate_object(self) -> Issuer:
+        return Issuer(
+            self.api,
+            {
+                "apiVersion": self._obj_kind.version,
+                "kind": self._obj_kind.kind,
+                "metadata": {
+                    "name": self._obj_name,
+                    "namespace": self._obj_namespace,
+                },
+                "spec": self._spec,
+            },
+        )
+
+    @classmethod
+    def from_config(cls, config: config.Issuer) -> list:
+        objects = []
+
+        if config.type == "acme":
+            spec = {
+                "acme": {
+                    "email": config.email,
+                    "server": config.server,
+                    "privateKeySecretRef": {
+                        "name": "cert-manager-issuer-account-key",
+                    },
+                },
+            }
+
+            if config.solver.type == "http":
+                spec["acme"]["solvers"] = [
+                    {
+                        "http01": {
+                            "ingress": {
+                                "class": "openstack",
+                            },
+                        },
+                    },
+                ]
+            elif config.solver.type == "rfc2136":
+                # NOTE(mnaser): We have to create a secret containing the AWS
+                #               credentials in this case.
+                objects.append(
+                    v1.ApplySecretTask(
+                        constants.NAMESPACE_OPENSTACK,
+                        "cert-manager-issuer-tsig-secret-key",
+                        data={
+                            "tsig-secret-key": base64.encode_as_text(
+                                config.solver.tsig_secret
+                            ),
+                        },
+                    )
+                )
+
+                spec["acme"]["solvers"] = [
+                    {
+                        "dns01": {
+                            "rfc2136": {
+                                "nameserver": config.solver.nameserver,
+                                "tsigAlgorithm": config.solver.tsig_algorithm,
+                                "tsigKeyName": config.solver.tsig_key_name,
+                                "tsigSecretSecretRef": {
+                                    "name": "cert-manager-issuer-tsig-secret-key",
+                                    "key": "tsig-secret-key",
+                                },
+                            },
+                        },
+                    },
+                ]
+            elif config.solver.type == "route53":
+                # NOTE(mnaser): We have to create a secret containing the AWS
+                #               credentials in this case.
+                objects.append(
+                    v1.ApplySecretTask(
+                        constants.NAMESPACE_OPENSTACK,
+                        "cert-manager-issuer-route53-credentials",
+                        data={
+                            "secret-access-key": base64.encode_as_text(
+                                config.solver.secret_access_key
+                            ),
+                        },
+                    )
+                )
+
+                spec["acme"]["solvers"] = [
+                    {
+                        "dns01": {
+                            "route53": {
+                                "region": config.solver.region,
+                                "hostedZoneID": config.solver.hosted_zone_id,
+                                "accessKeyID": config.solver.access_key_id,
+                                "secretAccessKeySecretRef": {
+                                    "name": "cert-manager-issuer-route53-credentials",
+                                    "key": "secret-access-key",
+                                },
+                            },
+                        },
+                    },
+                ]
+        elif config.type == "ca":
+            # NOTE(mnaser): We have to create a secret containing the CA
+            #               certificate and key in this case.
+            objects.append(
+                v1.ApplySecretTask(
+                    constants.NAMESPACE_OPENSTACK,
+                    "cert-manager-issuer-ca",
+                    data={
+                        "tls.crt": base64.encode_as_text(config.certificate),
+                        "tls.key": base64.encode_as_text(config.private_key),
+                    },
+                )
+            )
+
+            spec = {
+                "ca": {
+                    "secretName": "cert-manager-issuer-ca",
+                }
+            }
+        elif config.type == "self-signed":
+            # NOTE(mnaser): We have to setup the self-signed CA in this case
+            objects += [
+                ApplyIssuerTask(
+                    namespace=constants.NAMESPACE_OPENSTACK,
+                    name="self-signed",
+                    spec={
+                        "selfSigned": {},
+                    },
+                ),
+                ApplyCertificateTask(
+                    namespace=constants.NAMESPACE_OPENSTACK,
+                    name="self-signed-ca",
+                    spec={
+                        "isCA": True,
+                        "commonName": "selfsigned-ca",
+                        "secretName": "cert-manager-selfsigned-ca",
+                        "duration": "86400h",
+                        "renewBefore": "360h",
+                        "privateKey": {"algorithm": "ECDSA", "size": 256},
+                        "issuerRef": {
+                            "name": "self-signed",
+                            "namespace": constants.NAMESPACE_OPENSTACK,
+                            "kind": "Issuer",
+                            "group": "cert-manager.io",
+                        },
+                    },
+                ),
+            ]
+
+            spec = {
+                "ca": {
+                    "secretName": "cert-manager-selfsigned-ca",
+                }
+            }
+
+        return objects + [
+            ApplyIssuerTask(
+                namespace=constants.NAMESPACE_OPENSTACK, name="openstack", spec=spec
+            )
+        ]
diff --git a/atmosphere/tasks/kubernetes/flux.py b/atmosphere/tasks/kubernetes/flux.py
index fb66487..be3edcb 100644
--- a/atmosphere/tasks/kubernetes/flux.py
+++ b/atmosphere/tasks/kubernetes/flux.py
@@ -15,32 +15,29 @@
 
 
 class ApplyHelmRepositoryTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, namespace: str, name: str, url: str, *args, **kwargs):
+    def __init__(self, namespace: str, name: str, url: str):
+        self._url = url
+
         super().__init__(
-            HelmRepository,
-            namespace,
-            name,
-            requires=set(["namespace", "name", "url"]),
-            inject={"name": name, "url": url},
-            *args,
-            **kwargs,
+            kind=HelmRepository,
+            namespace=namespace,
+            name=name,
+            requires=set(["namespace"]),
         )
 
-    def generate_object(
-        self, namespace: pykube.Namespace, name: str, url: str, *args, **kwargs
-    ):
+    def generate_object(self) -> HelmRepository:
         return HelmRepository(
             self.api,
             {
-                "apiVersion": "source.toolkit.fluxcd.io/v1beta2",
-                "kind": "HelmRepository",
+                "apiVersion": self._obj_kind.version,
+                "kind": self._obj_kind.kind,
                 "metadata": {
-                    "name": name,
-                    "namespace": namespace.name,
+                    "name": self._obj_name,
+                    "namespace": self._obj_namespace,
                 },
                 "spec": {
                     "interval": "1m",
-                    "url": url,
+                    "url": self._url,
                 },
             },
         )
@@ -65,68 +62,43 @@
         *args,
         **kwargs,
     ):
+        self._repository = repository
+        self._chart = chart
+        self._version = version
+        self._values = values
+        self._values_from = values_from
+
         kwargs.setdefault("requires", set())
-        kwargs["requires"] = kwargs["requires"].union(
-            set(
-                [
-                    "namespace",
-                    "name",
-                    "repository",
-                    "chart",
-                    "version",
-                    "values",
-                    "values_from",
-                ]
-            )
-        )
+        kwargs["requires"] = kwargs["requires"].union(set(["repository"]))
 
         super().__init__(
-            HelmRelease,
-            namespace,
-            name,
+            kind=HelmRelease,
+            namespace=namespace,
+            name=name,
             rebind={"repository": f"helm-repository-{namespace}-{repository}"},
-            inject={
-                "name": name,
-                "repository": repository,
-                "chart": chart,
-                "version": version,
-                "values": values,
-                "values_from": values_from,
-            },
             *args,
             **kwargs,
         )
 
-    def generate_object(
-        self,
-        namespace: pykube.Namespace,
-        name: str,
-        repository: HelmRepository,
-        chart: str,
-        version: str,
-        values: dict,
-        values_from: list,
-        *args,
-        **kwargs,
-    ) -> HelmRelease:
+    def generate_object(self) -> HelmRelease:
         return HelmRelease(
             self.api,
             {
-                "apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
-                "kind": "HelmRelease",
+                "apiVersion": self._obj_kind.version,
+                "kind": self._obj_kind.kind,
                 "metadata": {
-                    "name": name,
-                    "namespace": namespace.name,
+                    "name": self._obj_name,
+                    "namespace": self._obj_namespace,
                 },
                 "spec": {
                     "interval": "60s",
                     "chart": {
                         "spec": {
-                            "chart": chart,
-                            "version": version,
+                            "chart": self._chart,
+                            "version": self._version,
                             "sourceRef": {
                                 "kind": "HelmRepository",
-                                "name": repository.name,
+                                "name": self._repository,
                             },
                         }
                     },
@@ -136,8 +108,8 @@
                     "upgrade": {
                         "disableWait": True,
                     },
-                    "values": values,
-                    "valuesFrom": values_from,
+                    "values": self._values,
+                    "valuesFrom": self._values_from,
                 },
             },
         )
diff --git a/atmosphere/tasks/kubernetes/v1.py b/atmosphere/tasks/kubernetes/v1.py
index d183d0c..fa205ae 100644
--- a/atmosphere/tasks/kubernetes/v1.py
+++ b/atmosphere/tasks/kubernetes/v1.py
@@ -7,86 +7,69 @@
 
 
 class ApplyNamespaceTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, name: str, *args, **kwargs):
-        super().__init__(
-            pykube.Namespace,
-            None,
-            name,
-            requires=set(["name"]),
-            inject={"name": name},
-            *args,
-            **kwargs,
-        )
+    def __init__(self, name: str):
+        super().__init__(kind=pykube.Namespace, namespace=None, name=name)
 
-    def generate_object(self, name, *args, **kwargs):
+    def generate_object(self) -> pykube.Namespace:
         return pykube.Namespace(
             self.api,
             {
-                "apiVersion": "v1",
-                "kind": "Namespace",
-                "metadata": {"name": name},
+                "apiVersion": self._obj_kind.version,
+                "kind": self._obj_kind.kind,
+                "metadata": {"name": self._obj_name},
             },
         )
 
 
 class ApplyServiceTask(base.ApplyKubernetesObjectTask):
     def __init__(self, namespace: str, name: str, labels: dict, spec: dict):
+        self._labels = labels
+        self._spec = spec
+
         super().__init__(
-            pykube.Service,
-            namespace,
-            name,
-            requires=set(["namespace", "name", "labels", "spec"]),
-            inject={"name": name, "labels": labels, "spec": spec},
+            kind=pykube.Service,
+            namespace=namespace,
+            name=name,
+            requires=set(["namespace"]),
         )
 
-    def generate_object(
-        self,
-        namespace: pykube.Namespace,
-        name: str,
-        labels: dict,
-        spec: dict,
-        *args,
-        **kwargs,
-    ) -> pykube.Service:
+    def generate_object(self) -> pykube.Service:
         return pykube.Service(
             self.api,
             {
-                "apiVersion": "v1",
-                "kind": "Service",
+                "apiVersion": self._obj_kind.version,
+                "kind": self._obj_kind.kind,
                 "metadata": {
-                    "name": name,
-                    "namespace": namespace.name,
-                    "labels": labels,
+                    "name": self._obj_name,
+                    "namespace": self._obj_namespace,
+                    "labels": self._labels,
                 },
-                "spec": spec,
+                "spec": self._spec,
             },
         )
 
 
 class ApplySecretTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, namespace: str, name: str, data: str, *args, **kwargs):
+    def __init__(self, namespace: str, name: str, data: str):
+        self._data = data
+
         super().__init__(
-            pykube.Secret,
-            namespace,
-            name,
-            requires=set(["namespace", "name", "data"]),
-            inject={"name": name, "data": data},
-            *args,
-            **kwargs,
+            kind=pykube.Secret,
+            namespace=namespace,
+            name=name,
+            requires=set(["namespace"]),
         )
 
-    def generate_object(
-        self, namespace: pykube.Namespace, name: str, data: dict, *args, **kwargs
-    ):
+    def generate_object(self) -> pykube.Secret:
         return pykube.Secret(
             self.api,
             {
                 "apiVersion": "v1",
                 "kind": "Secret",
                 "metadata": {
-                    "name": name,
-                    "namespace": namespace.name,
+                    "name": self._obj_name,
+                    "namespace": self._obj_namespace,
                 },
-                "data": data,
+                "data": self._data,
             },
         )
diff --git a/atmosphere/tests/e2e/test_operator.py b/atmosphere/tests/e2e/test_operator.py
index 567e5a7..65d03ee 100644
--- a/atmosphere/tests/e2e/test_operator.py
+++ b/atmosphere/tests/e2e/test_operator.py
@@ -35,7 +35,10 @@
         "atmosphere_config": {
             "memcached": {
                 "secret_key": "foobar",
-            }
+            },
+            "issuer": {
+                "email": "test@example.com",
+            },
         },
     }
 
diff --git a/atmosphere/tests/unit/tasks/kubernetes/test_cert_manager.py b/atmosphere/tests/unit/tasks/kubernetes/test_cert_manager.py
new file mode 100644
index 0000000..870f443
--- /dev/null
+++ b/atmosphere/tests/unit/tasks/kubernetes/test_cert_manager.py
@@ -0,0 +1,310 @@
+import textwrap
+
+import pykube
+import pytest
+from oslo_serialization import base64
+
+from atmosphere.models import config
+from atmosphere.tasks import constants
+from atmosphere.tasks.kubernetes import cert_manager
+
+
+@pytest.mark.parametrize(
+    "cfg_data,expected",
+    [
+        pytest.param(
+            textwrap.dedent(
+                """\
+                [issuer]
+                email = "mnaser@vexxhost.com"
+                """
+            ),
+            [
+                {
+                    "apiVersion": cert_manager.Issuer.version,
+                    "kind": cert_manager.Issuer.kind,
+                    "metadata": {
+                        "name": "openstack",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "spec": {
+                        "acme": {
+                            "email": "mnaser@vexxhost.com",
+                            "server": "https://acme-v02.api.letsencrypt.org/directory",
+                            "privateKeySecretRef": {
+                                "name": "cert-manager-issuer-account-key",
+                            },
+                            "solvers": [
+                                {
+                                    "http01": {
+                                        "ingress": {
+                                            "class": "openstack",
+                                        },
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                },
+            ],
+            id="default",
+        ),
+        pytest.param(
+            textwrap.dedent(
+                """\
+                [issuer]
+                email = "mnaser@vexxhost.com"
+
+                [issuer.solver]
+                type = "rfc2136"
+                nameserver = "1.2.3.4:53"
+                tsig_algorithm = "hmac-sha256"
+                tsig_key_name = "foobar"
+                tsig_secret = "secret123"
+                """
+            ),
+            [
+                {
+                    "apiVersion": pykube.Secret.version,
+                    "kind": pykube.Secret.kind,
+                    "metadata": {
+                        "name": "cert-manager-issuer-tsig-secret-key",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "data": {
+                        "tsig-secret-key": base64.encode_as_text("secret123"),
+                    },
+                },
+                {
+                    "apiVersion": cert_manager.Issuer.version,
+                    "kind": cert_manager.Issuer.kind,
+                    "metadata": {
+                        "name": "openstack",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "spec": {
+                        "acme": {
+                            "email": "mnaser@vexxhost.com",
+                            "server": "https://acme-v02.api.letsencrypt.org/directory",
+                            "privateKeySecretRef": {
+                                "name": "cert-manager-issuer-account-key",
+                            },
+                            "solvers": [
+                                {
+                                    "dns01": {
+                                        "rfc2136": {
+                                            "nameserver": "1.2.3.4:53",
+                                            "tsigAlgorithm": "hmac-sha256",
+                                            "tsigKeyName": "foobar",
+                                            "tsigSecretSecretRef": {
+                                                "name": "cert-manager-issuer-tsig-secret-key",
+                                                "key": "tsig-secret-key",
+                                            },
+                                        },
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                },
+            ],
+            id="rfc2136",
+        ),
+        pytest.param(
+            textwrap.dedent(
+                """\
+                [issuer]
+                email = "mnaser@vexxhost.com"
+
+                [issuer.solver]
+                type = "route53"
+                hosted_zone_id = "Z3A4X2Y2Y3"
+                access_key_id = "AKIAIOSFODNN7EXAMPLE"
+                secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
+                """
+            ),
+            [
+                {
+                    "apiVersion": pykube.Secret.version,
+                    "kind": pykube.Secret.kind,
+                    "metadata": {
+                        "name": "cert-manager-issuer-route53-credentials",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "data": {
+                        "secret-access-key": base64.encode_as_text(
+                            "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
+                        ),
+                    },
+                },
+                {
+                    "apiVersion": cert_manager.Issuer.version,
+                    "kind": cert_manager.Issuer.kind,
+                    "metadata": {
+                        "name": "openstack",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "spec": {
+                        "acme": {
+                            "email": "mnaser@vexxhost.com",
+                            "server": "https://acme-v02.api.letsencrypt.org/directory",
+                            "privateKeySecretRef": {
+                                "name": "cert-manager-issuer-account-key",
+                            },
+                            "solvers": [
+                                {
+                                    "dns01": {
+                                        "route53": {
+                                            "region": "global",
+                                            "hostedZoneID": "Z3A4X2Y2Y3",
+                                            "accessKeyID": "AKIAIOSFODNN7EXAMPLE",
+                                            "secretAccessKeySecretRef": {
+                                                "name": "cert-manager-issuer-route53-credentials",
+                                                "key": "secret-access-key",
+                                            },
+                                        },
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                },
+            ],
+            id="route53",
+        ),
+        pytest.param(
+            textwrap.dedent(
+                """\
+                [issuer]
+                type = "ca"
+                certificate = '''
+                -----BEGIN CERTIFICATE-----
+                MIIDBjCCAe4CCQDQ3Z0Z2Z0Z0jANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC
+                VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x
+                ...
+                -----END CERTIFICATE-----
+                '''
+                private_key = '''
+                -----BEGIN RSA PRIVATE KEY-----
+                MIIEpAIBAAKCAQEAw3Z0Z2Z0Z0jANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC
+                VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x
+                ...
+                -----END RSA PRIVATE KEY-----
+                '''
+                """
+            ),
+            [
+                {
+                    "apiVersion": pykube.Secret.version,
+                    "kind": pykube.Secret.kind,
+                    "metadata": {
+                        "name": "cert-manager-issuer-ca",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "data": {
+                        "tls.crt": base64.encode_as_text(
+                            textwrap.dedent(
+                                """\
+                                -----BEGIN CERTIFICATE-----
+                                MIIDBjCCAe4CCQDQ3Z0Z2Z0Z0jANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC
+                                VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x
+                                ...
+                                -----END CERTIFICATE-----
+                                """
+                            )
+                        ),
+                        "tls.key": base64.encode_as_text(
+                            textwrap.dedent(
+                                """\
+                                -----BEGIN RSA PRIVATE KEY-----
+                                MIIEpAIBAAKCAQEAw3Z0Z2Z0Z0jANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC
+                                VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x
+                                ...
+                                -----END RSA PRIVATE KEY-----
+                                """
+                            )
+                        ),
+                    },
+                },
+                {
+                    "apiVersion": cert_manager.Issuer.version,
+                    "kind": cert_manager.Issuer.kind,
+                    "metadata": {
+                        "name": "openstack",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "spec": {
+                        "ca": {
+                            "secretName": "cert-manager-issuer-ca",
+                        },
+                    },
+                },
+            ],
+            id="ca",
+        ),
+        pytest.param(
+            textwrap.dedent(
+                """\
+                [issuer]
+                type = "self-signed"
+                """
+            ),
+            [
+                {
+                    "apiVersion": cert_manager.Issuer.version,
+                    "kind": cert_manager.Issuer.kind,
+                    "metadata": {
+                        "name": "self-signed",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "spec": {
+                        "selfSigned": {},
+                    },
+                },
+                {
+                    "apiVersion": cert_manager.Certificate.version,
+                    "kind": cert_manager.Certificate.kind,
+                    "metadata": {
+                        "name": "self-signed-ca",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "spec": {
+                        "isCA": True,
+                        "commonName": "selfsigned-ca",
+                        "secretName": "cert-manager-selfsigned-ca",
+                        "duration": "86400h",
+                        "renewBefore": "360h",
+                        "privateKey": {"algorithm": "ECDSA", "size": 256},
+                        "issuerRef": {
+                            "name": "self-signed",
+                            "namespace": constants.NAMESPACE_OPENSTACK,
+                            "kind": "Issuer",
+                            "group": "cert-manager.io",
+                        },
+                    },
+                },
+                {
+                    "apiVersion": cert_manager.Issuer.version,
+                    "kind": cert_manager.Issuer.kind,
+                    "metadata": {
+                        "name": "openstack",
+                        "namespace": constants.NAMESPACE_OPENSTACK,
+                    },
+                    "spec": {
+                        "ca": {
+                            "secretName": "cert-manager-selfsigned-ca",
+                        },
+                    },
+                },
+            ],
+            id="self-signed",
+        ),
+    ],
+)
+def test_apply_issuer_task_from_config(pykube, cfg_data, expected):
+    cfg = config.Config.from_string(cfg_data, validate=False)
+    cfg.issuer.validate()
+    assert [
+        t.generate_object().obj
+        for t in cert_manager.ApplyIssuerTask.from_config(cfg.issuer)
+    ] == expected
diff --git a/docs/certificates.md b/docs/certificates.md
index 8beefbe..7975116 100644
--- a/docs/certificates.md
+++ b/docs/certificates.md
@@ -1,88 +1,113 @@
 # Certificates
 
-## Using LetsEncrypt DNS challenges
+Atmosphere simplifies all the management of your SSL certificates for all of
+your API endpoints by automatically issuing and renewing certificates for you.
 
-### RFC2136
+## ACME
+
+Atmosphere uses the [ACME](https://tools.ietf.org/html/rfc8555) protocol by
+default to request certificates from [LetsEncrypt](https://letsencrypt.org/).
+
+This is configured to work out of the box if your APIs are publicly accessible,
+you just need to configure an email address.
+
+```yaml
+atmosphere_issuer_config:
+  email: foo@bar.com
+```
+
+If you're running your own internal ACME server, you can configure Atmosphere to
+point towards it by setting the `server` field.
+
+```yaml
+atmosphere_issuer_config:
+  server: https://acme.example.com
+  email: foo@bar.com
+```
+
+### DNS-01 challenges
+
+Atmosphere uses the `HTTP-01` solver by default, which means that as long as
+your ACME server can reach your API, you don't need to do anything else.
+
+If your ACME server cannot reach your API, you will need to use the DNS-01
+challenges which require you to configure your DNS provider.
+
+#### RFC2136
 
 If you have DNS server that supports RFC2136, you can use it to solve the DNS
-challenges, you'll need to have the following information:
-
-- Email address
-- Nameserver IP address
-- TSIG Algorithm
-- TSIG Key Name
-- TSIG Key Secret
-
-You'll need to update your Ansible inventory to be the following:
+challenges, you can use the following configuration:
 
 ```yaml
-cert_manager_issuer:
-  acme:
-    email: <EMAIL>
-    privateKeySecretRef:
-      name: letsencrypt-prod
-    server: https://acme-v02.api.letsencrypt.org/directory
-    solvers:
-    - dns01:
-        rfc2136:
-          nameserver: <NS>:<PORT>
-          tsigAlgorithm: <ALGORITHM>
-          tsigKeyName: <NAME>
-          tsigSecretSecretRef:
-            key: tsig-secret-key
-            name: tsig-secret
+atmosphere_issuer_config:
+  solver:
+    type: rfc2136
+    nameserver: <NAMESERVER>:<PORT>
+    tsig_algorithm: <ALGORITHM>
+    tsig_key_name: <NAME>
+    tsig_secret: <SECRET>
 ```
 
-After you're done, you'll need to add a new secret to the Kubernetes cluster,
-you will need to do it by using the following YAML file:
+#### Route53
+
+If you are using Route53 to host the DNS for your domains, you can use the
+following configuration:
 
 ```yaml
-apiVersion: v1
-kind: Secret
-metadata:
-  name: tsig-secret
-  namespace: openstack
-type: Opaque
-stringData:
-  tsig-secret-key: <KEY>
+atmosphere_issuer_config:
+  email: foo@bar.com
+  solver:
+    type: route53
+    hosted_zone_id: <HOSTED_ZONE_ID>
+    access_key_id: <AWS_ACCESS_KEY_ID>
+    secret_access_key: <AWS_SECRET_ACCESS_KEY>
 ```
 
-## Using self-signed certificates
+!!! note
+
+    You'll need to make sure that your AWS credentials have the correct
+    permissions to update the Route53 zone.
+
+## Using pre-existing CA
+
+If you have an existing CA that you'd like to use with Atmosphere, you can
+simply configure it by including the certificate and private key:
+
+```yaml
+atmosphere_issuer_config:
+  type: ca
+  certificate: |
+    -----BEGIN CERTIFICATE-----
+    MIIDBjCCAe4CCQDQ3Z0Z2Z0Z0jANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC
+    VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x
+    ...
+    -----END CERTIFICATE-----
+  private_key: |
+    -----BEGIN RSA PRIVATE KEY-----
+    MIIEpAIBAAKCAQEAw3Z0Z2Z0Z0jANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC
+    VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28x
+    ...
+    -----END RSA PRIVATE KEY-----
+```
+
+!!! note
+
+    If your issuer is an intermediate certificate, you will need to ensure that
+    they `certificate` key includes the full chain in the correct order of issuer,
+    intermediate(s) then root.
+
+## Self-signed certificates
 
 If you are in an environment which does not have a trusted certificate authority
 and it does not have access to the internet to be able to use LetsEncrypt, you
 can use self-signed certificates by adding the following to your inventory:
 
 ```yaml
-cert_manager_issuer:
-  ca:
-    secretName: root-secret
+atmosphere_issuer_config:
+  type: self-signed
 ```
 
-## Using pre-existing CA
+!!! warning
 
-If you have your own CA and want to use it, you will need to update your Ansible inventory to be the following:
-
-```yaml
-cert_manager_issuer:
-  ca:
-    secretName: custom-openstack-ca-key-pair
-```
-
-After you're done, you'll need to add a new secret to the Kubernetes cluster,
-you will need to do it by using the following YAML file:
-
-```yaml
-apiVersion: v1
-kind: Secret
-metadata:
-  name: custom-openstack-ca-key-pair
-  namespace: openstack
-type: Opaque
-stringData:
-  tls.crt: |
-    CA_CERTIFICATE_HERE
-  tls.key: |
-    CA_PRIVATE_KEY_HERE
-```
-NOTE: If your issuer represents an intermediate, ensure that tls.crt contains the issuer's full chain in the correct order: issuer -> intermediate(s) -> root.
+    Self-signed certificates are not recommended for production environments,
+    they are only recommended for development and testing environments.
diff --git a/molecule/default/group_vars/all/molecule.yml b/molecule/default/group_vars/all/molecule.yml
index eed6fd7..cb7ce00 100644
--- a/molecule/default/group_vars/all/molecule.yml
+++ b/molecule/default/group_vars/all/molecule.yml
@@ -1,8 +1,7 @@
 atmosphere_image: "{{ lookup('file', lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') + '/image') }}"

 

-cert_manager_issuer:

-  ca:

-    secretName: root-secret

+atmosphere_issuer_config:

+  type: self-signed

 

 openstack_helm_glance_images:

   - name: cirros

diff --git a/playbooks/openstack.yml b/playbooks/openstack.yml
index 953551a..8fa8ece 100644
--- a/playbooks/openstack.yml
+++ b/playbooks/openstack.yml
@@ -52,10 +52,6 @@
       tags:
         - prometheus-pushgateway
 
-    - role: cert_manager
-      tags:
-        - cert-manager
-
     - role: keepalived
       tags:
         - keepalived
diff --git a/roles/atmosphere/defaults/main.yml b/roles/atmosphere/defaults/main.yml
index 5f20539..79c15e7 100644
--- a/roles/atmosphere/defaults/main.yml
+++ b/roles/atmosphere/defaults/main.yml
@@ -4,3 +4,4 @@
   memcached:
     secret_key: "{{ openstack_helm_endpoints_memcached_secret_key }}"
     overrides: "{{ openstack_helm_infra_memcached_values | default({}) }}"
+  issuer: "{{ atmosphere_issuer_config }}"
diff --git a/roles/atmosphere/templates/role.yml b/roles/atmosphere/templates/role.yml
index 47a0361..1a0a38e 100644
--- a/roles/atmosphere/templates/role.yml
+++ b/roles/atmosphere/templates/role.yml
@@ -14,3 +14,6 @@
   - apiGroups: ["rabbitmq.com"]
     resources: ["rabbitmqclusters"]
     verbs: ["get", "create", "patch"]
+  - apiGroups: ["cert-manager.io"]
+    resources: ["certificates", "issuers"]
+    verbs: ["get", "create", "patch"]
diff --git a/roles/cert_manager/README.md b/roles/cert_manager/README.md
deleted file mode 100644
index 77865d4..0000000
--- a/roles/cert_manager/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# `cert_manager`
diff --git a/roles/cert_manager/defaults/main.yml b/roles/cert_manager/defaults/main.yml
deleted file mode 100644
index 183b22d..0000000
--- a/roles/cert_manager/defaults/main.yml
+++ /dev/null
@@ -1,35 +0,0 @@
----
-# .. vim: foldmarker=[[[,]]]:foldmethod=marker
-
-# .. Copyright (C) 2022 VEXXHOST, Inc.
-# .. SPDX-License-Identifier: Apache-2.0
-
-# Default variables
-# =================
-
-# .. contents:: Sections
-#    :local:
-
-
-# .. envvar:: cert_manager_acme_server [[[
-#
-# ACME server URL
-cert_manager_acme_server: "{{ lookup('env', 'ATMOSPHERE_ACME_SERVER') | default('https://acme-v02.api.letsencrypt.org/directory', True) }}"
-
-                                                                   # ]]]
-# .. envvar:: cert_manager_issuer [[[
-#
-# Definition for the ``cert-manager`` issuer
-# To use self-signed CA certificate, set cert_manager_issuer.ca.secretName as root-secret.
-cert_manager_issuer:
-  acme:
-    email: mnaser@vexxhost.com
-    server: "{{ cert_manager_acme_server }}"
-    privateKeySecretRef:
-      name: issuer-account-key
-    solvers:
-      - http01:
-          ingress:
-            class: openstack
-
-                                                                   # ]]]
diff --git a/roles/cert_manager/tasks/main.yml b/roles/cert_manager/tasks/main.yml
deleted file mode 100644
index c9c0901..0000000
--- a/roles/cert_manager/tasks/main.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# 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.
-
-- name: Create Issuer
-  kubernetes.core.k8s:
-    state: present
-    definition:
-      apiVersion: cert-manager.io/v1
-      kind: Issuer
-      metadata:
-        name: openstack
-        namespace: openstack
-      spec: "{{ cert_manager_issuer }}"
-  # NOTE(mnaser): Since we haven't moved to the operator pattern yet, we need to
-  #               keep retrying a few times as the CRDs might not be installed
-  #               yet.
-  retries: 60
-  delay: 5
-  register: _result
-  until: _result is not failed
-
-- name: Bootstrap self-signed PKI
-  block:
-    - name: Create self-signed issuer
-      kubernetes.core.k8s:
-        state: present
-        definition:
-          apiVersion: cert-manager.io/v1
-          kind: ClusterIssuer
-          metadata:
-            name: selfsigned-issuer
-          spec:
-            selfSigned: {}
-
-    - name: Bootstrap a custom root certificate for a private PKI
-      kubernetes.core.k8s:
-        state: present
-        definition:
-          apiVersion: cert-manager.io/v1
-          kind: Certificate
-          metadata:
-            name: selfsigned-ca
-            namespace: openstack
-          spec:
-            isCA: true
-            commonName: selfsigned-ca
-            secretName: root-secret
-            duration: 86400h # 3600d
-            renewBefore: 360h # 15d
-            privateKey:
-              algorithm: ECDSA
-              size: 256
-            issuerRef:
-              name: selfsigned-issuer
-              kind: ClusterIssuer
-              group: cert-manager.io
-
-    - name: Wait till the root secret is created
-      kubernetes.core.k8s_info:
-        api_version: v1
-        kind: Secret
-        wait: true
-        name: root-secret
-        namespace: openstack
-        wait_sleep: 10
-        wait_timeout: 300
-      register: _openstack_helm_root_secret
-
-    - name: Copy CA certificate on host
-      ansible.builtin.copy:
-        content: "{{ _openstack_helm_root_secret.resources[0].data['tls.crt'] | b64decode }}"
-        dest: "/usr/local/share/ca-certificates/self-signed-osh-ca.crt"
-        mode: "0644"
-
-    - name: Update ca certificates on host
-      ansible.builtin.command:
-        cmd: update-ca-certificates
-      changed_when: false
-  when:
-    - cert_manager_issuer.ca.secretName is defined
-    - cert_manager_issuer.ca.secretName == "root-secret"
diff --git a/roles/certificates/README.md b/roles/certificates/README.md
new file mode 100644
index 0000000..ab2904d
--- /dev/null
+++ b/roles/certificates/README.md
@@ -0,0 +1,6 @@
+# `certificates`
+
+!!! warning
+
+    This is a legacy role that is meant to be phased out eventually when there
+    is no need for the control-plane systems to have the CA installed on them.
diff --git a/roles/cert_manager/meta/main.yml b/roles/certificates/meta/main.yml
similarity index 92%
rename from roles/cert_manager/meta/main.yml
rename to roles/certificates/meta/main.yml
index 97154e4..4d6cbdd 100644
--- a/roles/cert_manager/meta/main.yml
+++ b/roles/certificates/meta/main.yml
@@ -14,7 +14,7 @@
 
 galaxy_info:
   author: VEXXHOST, Inc.
-  description: Ansible role for cert-manager
+  description: Ansible role for distributing certificates
   license: Apache-2.0
   min_ansible_version: 5.5.0
   standalone: false
diff --git a/roles/certificates/tasks/main.yml b/roles/certificates/tasks/main.yml
new file mode 100644
index 0000000..334d7e7
--- /dev/null
+++ b/roles/certificates/tasks/main.yml
@@ -0,0 +1,53 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# 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.
+
+- name: Bootstrap PKI
+  block:
+    - name: Wait till the secret is created
+      kubernetes.core.k8s_info:
+        api_version: v1
+        kind: Secret
+        name: cert-manager-selfsigned-ca
+        namespace: openstack
+        wait: true
+        wait_sleep: 1
+        wait_timeout: 300
+      register: _openstack_helm_root_secret
+      when: atomsphere_issuer_config.type == "self-signed"
+
+    - name: Wait till the secret is created
+      kubernetes.core.k8s_info:
+        api_version: v1
+        kind: Secret
+        name: cert-manager-issuer-ca
+        namespace: openstack
+        wait: true
+        wait_sleep: 1
+        wait_timeout: 300
+      register: _openstack_helm_root_secret
+      when: atomsphere_issuer_config.type == "ca"
+
+    - name: Copy CA certificate on host
+      ansible.builtin.copy:
+        content: "{{ _openstack_helm_root_secret.resources[0].data['tls.crt'] | b64decode }}"
+        dest: "/usr/local/share/ca-certificates/self-signed-osh-ca.crt"
+        mode: "0644"
+
+    - name: Update CA certificates on host
+      ansible.builtin.command:
+        cmd: update-ca-certificates
+      changed_when: false
+  when:
+    - atomsphere_issuer_config.type is defined
+    - atomsphere_issuer_config.type in ("self-signed", "ca")
diff --git a/roles/openstack_cli/meta/main.yml b/roles/openstack_cli/meta/main.yml
index c18c9db..d1ef07b 100644
--- a/roles/openstack_cli/meta/main.yml
+++ b/roles/openstack_cli/meta/main.yml
@@ -22,3 +22,6 @@
     - name: Ubuntu
       versions:
         - focal
+
+dependencies:
+  - role: certificates
diff --git a/roles/openstacksdk/meta/main.yml b/roles/openstacksdk/meta/main.yml
index b328fac..7d9c3db 100644
--- a/roles/openstacksdk/meta/main.yml
+++ b/roles/openstacksdk/meta/main.yml
@@ -22,3 +22,6 @@
     - name: Ubuntu
       versions:
         - focal
+
+dependencies:
+  - role: certificates