Skip to content

Commit

Permalink
Show file tree
Hide file tree
Showing 13 changed files with 693 additions and 185 deletions.
159 changes: 87 additions & 72 deletions lib/charms/mongodb/v0/mongodb_tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
from ops.framework import Object
from ops.model import ActiveStatus, MaintenanceStatus, Unit

from config import Config

APP_SCOPE = Config.Relations.APP_SCOPE
UNIT_SCOPE = Config.Relations.UNIT_SCOPE
Scopes = Config.Relations.Scopes


# The unique Charmhub library identifier, never change it
LIBID = "e02a50f0795e4dd292f58e93b4f493dd"

Expand All @@ -33,52 +40,57 @@
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 5

LIBPATCH = 6

logger = logging.getLogger(__name__)
TLS_RELATION = "certificates"


class MongoDBTLS(Object):
"""In this class we manage client database relations."""

def __init__(self, charm, peer_relation, substrate="k8s"):
def __init__(self, charm, peer_relation, substrate):
"""Manager of MongoDB client relations."""
super().__init__(charm, "client-relations")
self.charm = charm
self.substrate = substrate
self.peer_relation = peer_relation
self.certs = TLSCertificatesRequiresV1(self.charm, TLS_RELATION)
self.certs = TLSCertificatesRequiresV1(self.charm, Config.TLS.TLS_PEER_RELATION)
self.framework.observe(
self.charm.on.set_tls_private_key_action, self._on_set_tls_private_key
)
self.framework.observe(
self.charm.on[TLS_RELATION].relation_joined, self._on_tls_relation_joined
self.charm.on[Config.TLS.TLS_PEER_RELATION].relation_joined,
self._on_tls_relation_joined,
)
self.framework.observe(
self.charm.on[TLS_RELATION].relation_broken, self._on_tls_relation_broken
self.charm.on[Config.TLS.TLS_PEER_RELATION].relation_broken,
self._on_tls_relation_broken,
)
self.framework.observe(self.certs.on.certificate_available, self._on_certificate_available)
self.framework.observe(self.certs.on.certificate_expiring, self._on_certificate_expiring)

def is_tls_enabled(self, scope: Scopes):
"""Returns a boolean indicating if TLS for a given `scope` is enabled."""
return self.charm.get_secret(scope, Config.TLS.SECRET_CERT_LABEL) is not None

def _on_set_tls_private_key(self, event: ActionEvent) -> None:
"""Set the TLS private key, which will be used for requesting the certificate."""
logger.debug("Request to set TLS private key received.")
try:
self._request_certificate("unit", event.params.get("external-key", None))
self._request_certificate(UNIT_SCOPE, event.params.get("external-key", None))

if not self.charm.unit.is_leader():
event.log(
"Only juju leader unit can set private key for the internal certificate. Skipping."
)
return

self._request_certificate("app", event.params.get("internal-key", None))
self._request_certificate(APP_SCOPE, event.params.get("internal-key", None))
logger.debug("Successfully set TLS private key.")
except ValueError as e:
event.fail(str(e))

def _request_certificate(self, scope: str, param: Optional[str]):
def _request_certificate(self, scope: Scopes, param: Optional[str]):
if param is None:
key = generate_private_key()
else:
Expand All @@ -92,11 +104,11 @@ def _request_certificate(self, scope: str, param: Optional[str]):
sans_ip=[str(self.charm.model.get_binding(self.peer_relation).network.bind_address)],
)

self.charm.set_secret(scope, "key", key.decode("utf-8"))
self.charm.set_secret(scope, "csr", csr.decode("utf-8"))
self.charm.set_secret(scope, "cert", None)
self.charm.set_secret(scope, Config.TLS.SECRET_KEY_LABEL, key.decode("utf-8"))
self.charm.set_secret(scope, Config.TLS.SECRET_CSR_LABEL, csr.decode("utf-8"))
self.charm.set_secret(scope, Config.TLS.SECRET_CERT_LABEL, None)

if self.charm.model.get_relation(TLS_RELATION):
if self.charm.model.get_relation(Config.TLS.TLS_PEER_RELATION):
self.certs.request_certificate_creation(certificate_signing_request=csr)

@staticmethod
Expand All @@ -117,63 +129,59 @@ def _parse_tls_file(raw_content: str) -> bytes:
def _on_tls_relation_joined(self, _: RelationJoinedEvent) -> None:
"""Request certificate when TLS relation joined."""
if self.charm.unit.is_leader():
self._request_certificate("app", None)
self._request_certificate(APP_SCOPE, None)

self._request_certificate("unit", None)
self._request_certificate(UNIT_SCOPE, None)

def _on_tls_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Disable TLS when TLS relation broken."""
logger.debug("Disabling external TLS for unit: %s", self.charm.unit.name)
self.charm.set_secret("unit", "ca", None)
self.charm.set_secret("unit", "cert", None)
self.charm.set_secret("unit", "chain", None)
self.charm.set_secret(UNIT_SCOPE, Config.TLS.SECRET_CA_LABEL, None)
self.charm.set_secret(UNIT_SCOPE, Config.TLS.SECRET_CERT_LABEL, None)
self.charm.set_secret(UNIT_SCOPE, Config.TLS.SECRET_CHAIN_LABEL, None)

if self.charm.unit.is_leader():
logger.debug("Disabling internal TLS")
self.charm.set_secret("app", "ca", None)
self.charm.set_secret("app", "cert", None)
self.charm.set_secret("app", "chain", None)
if self.charm.get_secret("app", "cert"):
self.charm.set_secret(APP_SCOPE, Config.TLS.SECRET_CA_LABEL, None)
self.charm.set_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL, None)
self.charm.set_secret(APP_SCOPE, Config.TLS.SECRET_CHAIN_LABEL, None)

if self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL):
logger.debug(
"Defer until the leader deletes the internal TLS certificate to avoid second restart."
)
event.defer()
return

logger.debug("Restarting mongod with TLS disabled.")
if self.substrate == "vm":
self.charm.unit.status = MaintenanceStatus("disabling TLS")
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()
else:
self.charm.on_mongod_pebble_ready(event)
logger.info("Restarting mongod with TLS disabled.")
self.charm.unit.status = MaintenanceStatus("disabling TLS")
self.charm.delete_tls_certificate_from_workload()
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()

def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
"""Enable TLS when TLS certificate available."""
if (
event.certificate_signing_request.rstrip()
== self.charm.get_secret("unit", "csr").rstrip()
):
unit_csr = self.charm.get_secret(UNIT_SCOPE, Config.TLS.SECRET_CSR_LABEL)
app_csr = self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CSR_LABEL)

if unit_csr and event.certificate_signing_request.rstrip() == unit_csr.rstrip():
logger.debug("The external TLS certificate available.")
scope = "unit" # external crs
elif (
event.certificate_signing_request.rstrip()
== self.charm.get_secret("app", "csr").rstrip()
):
scope = UNIT_SCOPE # external crs
elif app_csr and event.certificate_signing_request.rstrip() == app_csr.rstrip():
logger.debug("The internal TLS certificate available.")
scope = "app" # internal crs
scope = APP_SCOPE # internal crs
else:
logger.error("An unknown certificate available.")
logger.error("An unknown certificate is available -- ignoring.")
return

old_cert = self.charm.get_secret(scope, "cert")
renewal = old_cert and old_cert != event.certificate

if scope == "unit" or (scope == "app" and self.charm.unit.is_leader()):
if scope == UNIT_SCOPE or (scope == APP_SCOPE and self.charm.unit.is_leader()):
self.charm.set_secret(
scope, "chain", "\n".join(event.chain) if event.chain is not None else None
scope,
Config.TLS.SECRET_CHAIN_LABEL,
"\n".join(event.chain) if event.chain is not None else None,
)
self.charm.set_secret(scope, "cert", event.certificate)
self.charm.set_secret(scope, "ca", event.ca)
self.charm.set_secret(scope, Config.TLS.SECRET_CERT_LABEL, event.certificate)
self.charm.set_secret(scope, Config.TLS.SECRET_CA_LABEL, event.ca)

if self._waiting_for_certs():
logger.debug(
Expand All @@ -182,46 +190,48 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
event.defer()
return

if renewal and self.substrate == "k8s":
self.charm.unit.get_container("mongod").stop("mongod")
logger.info("Restarting mongod with TLS enabled.")

logger.debug("Restarting mongod with TLS enabled.")
if self.substrate == "vm":
self.charm._push_tls_certificate_to_workload()
self.charm.unit.status = MaintenanceStatus("enabling TLS")
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()
else:
self.charm.on_mongod_pebble_ready(event)
self.charm.delete_tls_certificate_from_workload()
self.charm.push_tls_certificate_to_workload()
self.charm.unit.status = MaintenanceStatus("enabling TLS")
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()

def _waiting_for_certs(self):
"""Returns a boolean indicating whether additional certs are needed."""
if not self.charm.get_secret("app", "cert"):
if not self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL):
logger.debug("Waiting for application certificate.")
return True
if not self.charm.get_secret("unit", "cert"):
if not self.charm.get_secret(UNIT_SCOPE, Config.TLS.SECRET_CERT_LABEL):
logger.debug("Waiting for application certificate.")
return True

return False

def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
"""Request the new certificate when old certificate is expiring."""
if event.certificate.rstrip() == self.charm.get_secret("unit", "cert").rstrip():
if (
event.certificate.rstrip()
== self.charm.get_secret(UNIT_SCOPE, Config.TLS.SECRET_CERT_LABEL).rstrip()
):
logger.debug("The external TLS certificate expiring.")
scope = "unit" # external cert
elif event.certificate.rstrip() == self.charm.get_secret("app", "cert").rstrip():
scope = UNIT_SCOPE # external cert
elif (
event.certificate.rstrip()
== self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL).rstrip()
):
logger.debug("The internal TLS certificate expiring.")
if not self.charm.unit.is_leader():
return
scope = "app" # internal cert
scope = APP_SCOPE # internal cert
else:
logger.error("An unknown certificate expiring.")
return

logger.debug("Generating a new Certificate Signing Request.")
key = self.charm.get_secret(scope, "key").encode("utf-8")
old_csr = self.charm.get_secret(scope, "csr").encode("utf-8")
key = self.charm.get_secret(scope, Config.TLS.SECRET_KEY_LABEL).encode("utf-8")
old_csr = self.charm.get_secret(scope, Config.TLS.SECRET_CSR_LABEL).encode("utf-8")
new_csr = generate_csr(
private_key=key,
subject=self.get_host(self.charm.unit),
Expand All @@ -236,7 +246,7 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
new_certificate_signing_request=new_csr,
)

self.charm.set_secret(scope, "csr", new_csr.decode("utf-8"))
self.charm.set_secret(scope, Config.TLS.SECRET_CSR_LABEL, new_csr.decode("utf-8"))

def _get_sans(self) -> List[str]:
"""Create a list of DNS names for a MongoDB unit.
Expand All @@ -252,19 +262,24 @@ def _get_sans(self) -> List[str]:
str(self.charm.model.get_binding(self.peer_relation).network.bind_address),
]

def get_tls_files(self, scope: str) -> Tuple[Optional[str], Optional[str]]:
def get_tls_files(self, scope: Scopes) -> Tuple[Optional[str], Optional[str]]:
"""Prepare TLS files in special MongoDB way.
MongoDB needs two files:
— CA file should have a full chain.
— PEM file should have private key and certificate without certificate chain.
"""
ca = self.charm.get_secret(scope, "ca")
chain = self.charm.get_secret(scope, "chain")
if not self.is_tls_enabled(scope):
logging.debug(f"TLS disabled for {scope}")
return None, None
logging.debug(f"TLS *enabled* for {scope}, fetching data for CA and PEM files ")

ca = self.charm.get_secret(scope, Config.TLS.SECRET_CA_LABEL)
chain = self.charm.get_secret(scope, Config.TLS.SECRET_CHAIN_LABEL)
ca_file = chain if chain else ca

key = self.charm.get_secret(scope, "key")
cert = self.charm.get_secret(scope, "cert")
key = self.charm.get_secret(scope, Config.TLS.SECRET_KEY_LABEL)
cert = self.charm.get_secret(scope, Config.TLS.SECRET_CERT_LABEL)
pem_file = key
if cert:
pem_file = key + "\n" + cert if key else cert
Expand All @@ -276,4 +291,4 @@ def get_host(self, unit: Unit):
if self.substrate == "vm":
return self.charm._unit_ip(unit)
else:
return self.charm.get_hostname_by_unit(unit.name)
return self.charm.get_hostname_for_unit(unit)
Loading

0 comments on commit 711f6ae

Please sign in to comment.