Skip to content

Commit

Permalink
[DPE-2140] Juju 3.x secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
juditnovak authored Jul 21, 2023
1 parent 0d50315 commit a66e627
Show file tree
Hide file tree
Showing 16 changed files with 785 additions and 177 deletions.
124 changes: 74 additions & 50 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 @@ -36,7 +43,6 @@


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


class MongoDBTLS(Object):
Expand All @@ -48,37 +54,43 @@ def __init__(self, charm, peer_relation, substrate):
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):
"""Getting internal TLS flag (meaning)."""
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,22 +129,24 @@ 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."
)
Expand All @@ -147,28 +161,27 @@ def _on_tls_relation_broken(self, event: RelationBrokenEvent) -> None:

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

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 @@ -177,7 +190,7 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
event.defer()
return

logger.debug("Restarting mongod with TLS enabled.")
logger.info("Restarting mongod with TLS enabled.")

self.charm.push_tls_certificate_to_workload()
self.charm.unit.status = MaintenanceStatus("enabling TLS")
Expand All @@ -186,32 +199,38 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:

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 @@ -226,7 +245,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 @@ -242,19 +261,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 Down
33 changes: 32 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ codespell = "^2.2.4"
[tool.poetry.group.unit.dependencies]
coverage = {extras = ["toml"], version = "^7.2.7"}
pytest = "^7.3.1"
parameterized = "^0.9.0"

[tool.poetry.group.integration.dependencies]
lightkube = "^0.13.0"
pytest = "^7.3.1"
pytest-mock = "^3.11.1"
pytest-operator = "^0.27.0"
juju = "2.9.42.1 || 3.1.0.1"

Expand Down
Loading

0 comments on commit a66e627

Please sign in to comment.