Skip to content

Commit 9f81c03

Browse files
authored
Merge pull request #88 from canonical/update-tls-lib
fix issues with tls on immediate relation
2 parents c3e9b71 + 5676c37 commit 9f81c03

File tree

2 files changed

+101
-22
lines changed

2 files changed

+101
-22
lines changed

lib/charms/mongodb/v1/mongodb_tls.py

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
external relation.
99
"""
1010
import base64
11+
import json
1112
import logging
1213
import re
1314
import socket
14-
from typing import List, Optional, Tuple
15+
from typing import Optional, Tuple
1516

1617
from charms.tls_certificates_interface.v3.tls_certificates import (
1718
CertificateAvailableEvent,
@@ -20,6 +21,8 @@
2021
generate_csr,
2122
generate_private_key,
2223
)
24+
from cryptography import x509
25+
from cryptography.hazmat.backends import default_backend
2326
from ops.charm import ActionEvent, RelationBrokenEvent, RelationJoinedEvent
2427
from ops.framework import Object
2528
from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus
@@ -28,7 +31,8 @@
2831

2932
UNIT_SCOPE = Config.Relations.UNIT_SCOPE
3033
Scopes = Config.Relations.Scopes
31-
34+
SANS_DNS_KEY = "sans_dns"
35+
SANS_IPS_KEY = "sans_ips"
3236

3337
# The unique Charmhub library identifier, never change it
3438
LIBID = "e02a50f0795e4dd292f58e93b4f493dd"
@@ -38,7 +42,9 @@
3842

3943
# Increment this PATCH version before using `charmcraft publish-lib` or reset
4044
# to 0 if you are raising the major API version
41-
LIBPATCH = 2
45+
LIBPATCH = 5
46+
47+
WAIT_CERT_UPDATE = "wait-cert-updated"
4248

4349
logger = logging.getLogger(__name__)
4450

@@ -104,12 +110,13 @@ def request_certificate(
104110
else:
105111
key = self._parse_tls_file(param)
106112

113+
sans = self.get_new_sans()
107114
csr = generate_csr(
108115
private_key=key,
109116
subject=self._get_subject_name(),
110117
organization=self._get_subject_name(),
111-
sans=self._get_sans(),
112-
sans_ip=[str(self.charm.model.get_binding(self.peer_relation).network.bind_address)],
118+
sans=sans[SANS_DNS_KEY],
119+
sans_ip=sans[SANS_IPS_KEY],
113120
)
114121
self.set_tls_secret(internal, Config.TLS.SECRET_KEY_LABEL, key.decode("utf-8"))
115122
self.set_tls_secret(internal, Config.TLS.SECRET_CSR_LABEL, csr.decode("utf-8"))
@@ -118,9 +125,8 @@ def request_certificate(
118125
label = "int" if internal else "ext"
119126
self.charm.unit_peer_data[f"{label}_certs_subject"] = self._get_subject_name()
120127
self.charm.unit_peer_data[f"{label}_certs_subject"] = self._get_subject_name()
121-
122-
if self.charm.model.get_relation(Config.TLS.TLS_PEER_RELATION):
123-
self.certs.request_certificate_creation(certificate_signing_request=csr)
128+
self.certs.request_certificate_creation(certificate_signing_request=csr)
129+
self.set_waiting_for_cert_to_update(internal=internal, waiting=True)
124130

125131
@staticmethod
126132
def _parse_tls_file(raw_content: str) -> bytes:
@@ -158,12 +164,18 @@ def _on_tls_relation_joined(self, event: RelationJoinedEvent) -> None:
158164

159165
def _on_tls_relation_broken(self, event: RelationBrokenEvent) -> None:
160166
"""Disable TLS when TLS relation broken."""
161-
logger.debug("Disabling external and internal TLS for unit: %s", self.charm.unit.name)
167+
if not self.charm.db_initialised:
168+
logger.info("Deferring %s. db is not initialised.", str(type(event)))
169+
event.defer()
170+
return
171+
162172
if self.charm.upgrade_in_progress:
163173
logger.warning(
164174
"Disabling TLS is not supported during an upgrade. The charm may be in a broken, unrecoverable state."
165175
)
166176

177+
logger.debug("Disabling external and internal TLS for unit: %s", self.charm.unit.name)
178+
167179
for internal in [True, False]:
168180
self.set_tls_secret(internal, Config.TLS.SECRET_CA_LABEL, None)
169181
self.set_tls_secret(internal, Config.TLS.SECRET_CERT_LABEL, None)
@@ -188,6 +200,11 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
188200
event.defer()
189201
return
190202

203+
if not self.charm.db_initialised:
204+
logger.info("Deferring %s. db is not initialised.", str(type(event)))
205+
event.defer()
206+
return
207+
191208
int_csr = self.get_tls_secret(internal=True, label_name=Config.TLS.SECRET_CSR_LABEL)
192209
ext_csr = self.get_tls_secret(internal=False, label_name=Config.TLS.SECRET_CSR_LABEL)
193210

@@ -208,12 +225,13 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
208225
)
209226
self.set_tls_secret(internal, Config.TLS.SECRET_CERT_LABEL, event.certificate)
210227
self.set_tls_secret(internal, Config.TLS.SECRET_CA_LABEL, event.ca)
228+
self.set_waiting_for_cert_to_update(internal=internal, waiting=False)
211229

212230
if self.charm.is_role(Config.Role.CONFIG_SERVER) and internal:
213231
self.charm.cluster.update_ca_secret(new_ca=event.ca)
214232
self.charm.config_server.update_ca_secret(new_ca=event.ca)
215233

216-
if self.waiting_for_certs():
234+
if self.is_waiting_for_both_certs():
217235
logger.debug(
218236
"Defer till both internal and external TLS certificates available to avoid second restart."
219237
)
@@ -235,7 +253,7 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
235253
# clear waiting status if db service is ready
236254
self.charm.status.set_and_share_status(ActiveStatus())
237255

238-
def waiting_for_certs(self):
256+
def is_waiting_for_both_certs(self) -> bool:
239257
"""Returns a boolean indicating whether additional certs are needed."""
240258
if not self.get_tls_secret(internal=True, label_name=Config.TLS.SECRET_CERT_LABEL):
241259
logger.debug("Waiting for internal certificate.")
@@ -268,21 +286,25 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
268286
== self.get_tls_secret(internal=True, label_name=Config.TLS.SECRET_CERT_LABEL).rstrip()
269287
):
270288
logger.debug("The internal TLS certificate expiring.")
271-
272289
internal = True
273290
else:
274291
logger.error("An unknown certificate expiring.")
275292
return
276293

277294
logger.debug("Generating a new Certificate Signing Request.")
295+
self.request_new_certificates(internal)
296+
297+
def request_new_certificates(self, internal: bool) -> None:
298+
"""Requests the renewel of a new certificate."""
278299
key = self.get_tls_secret(internal, Config.TLS.SECRET_KEY_LABEL).encode("utf-8")
279300
old_csr = self.get_tls_secret(internal, Config.TLS.SECRET_CSR_LABEL).encode("utf-8")
301+
sans = self.get_new_sans()
280302
new_csr = generate_csr(
281303
private_key=key,
282304
subject=self._get_subject_name(),
283305
organization=self._get_subject_name(),
284-
sans=self._get_sans(),
285-
sans_ip=[str(self.charm.model.get_binding(self.peer_relation).network.bind_address)],
306+
sans=sans[SANS_DNS_KEY],
307+
sans_ip=sans[SANS_IPS_KEY],
286308
)
287309
logger.debug("Requesting a certificate renewal.")
288310

@@ -292,21 +314,53 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
292314
)
293315

294316
self.set_tls_secret(internal, Config.TLS.SECRET_CSR_LABEL, new_csr.decode("utf-8"))
317+
self.set_waiting_for_cert_to_update(waiting=True, internal=internal)
295318

296-
def _get_sans(self) -> List[str]:
319+
def get_new_sans(self) -> dict[str, list[str]]:
297320
"""Create a list of DNS names for a MongoDB unit.
298321
299322
Returns:
300323
A list representing the hostnames of the MongoDB unit.
301324
"""
302325
unit_id = self.charm.unit.name.split("/")[1]
303-
return [
304-
f"{self.charm.app.name}-{unit_id}",
305-
socket.getfqdn(),
306-
f"{self.charm.app.name}-{unit_id}.{self.charm.app.name}-endpoints",
307-
str(self.charm.model.get_binding(self.peer_relation).network.bind_address),
308-
"localhost",
309-
]
326+
327+
sans = {
328+
SANS_DNS_KEY: [
329+
f"{self.charm.app.name}-{unit_id}",
330+
socket.getfqdn(),
331+
"localhost",
332+
f"{self.charm.app.name}-{unit_id}.{self.charm.app.name}-endpoints",
333+
],
334+
SANS_IPS_KEY: [
335+
str(self.charm.model.get_binding(self.peer_relation).network.bind_address)
336+
],
337+
}
338+
339+
if self.charm.is_role(Config.Role.MONGOS) and self.charm.is_external_client:
340+
sans[SANS_IPS_KEY].append(
341+
self.charm.get_ext_mongos_host(self.charm.unit, incl_port=False)
342+
)
343+
344+
return sans
345+
346+
def get_current_sans(self, internal: bool) -> dict[str, list[str]] | None:
347+
"""Gets the current SANs for the unit cert."""
348+
# if unit has no certificates do not proceed.
349+
if not self.is_tls_enabled(internal=internal):
350+
return
351+
352+
pem_file = self.get_tls_secret(internal, Config.TLS.SECRET_CERT_LABEL)
353+
354+
try:
355+
cert = x509.load_pem_x509_certificate(pem_file.encode(), default_backend())
356+
sans = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
357+
sans_ip = [str(san) for san in sans.get_values_for_type(x509.IPAddress)]
358+
sans_dns = [str(san) for san in sans.get_values_for_type(x509.DNSName)]
359+
except x509.ExtensionNotFound:
360+
sans_ip = []
361+
sans_dns = []
362+
363+
return {SANS_IPS_KEY: sorted(sans_ip), SANS_DNS_KEY: sorted(sans_dns)}
310364

311365
def get_tls_files(self, internal: bool) -> Tuple[Optional[str], Optional[str]]:
312366
"""Prepare TLS files in special MongoDB way.
@@ -356,3 +410,23 @@ def _get_subject_name(self) -> str:
356410
return self.charm.get_config_server_name() or self.charm.app.name
357411

358412
return self.charm.app.name
413+
414+
def is_set_waiting_for_cert_to_update(
415+
self,
416+
internal: bool = False,
417+
) -> bool:
418+
"""Returns True if we are waiting for a cert to update."""
419+
scope = "int" if internal else "ext"
420+
label_name = f"{scope}-{WAIT_CERT_UPDATE}"
421+
422+
return json.loads(self.charm.unit_peer_data.get(label_name, "false"))
423+
424+
def set_waiting_for_cert_to_update(
425+
self,
426+
waiting: bool,
427+
internal: bool,
428+
) -> None:
429+
"""Sets a boolean indicator, for whether or not we are waiting for a cert to update."""
430+
scope = "int" if internal else "ext"
431+
label_name = f"{scope}-{WAIT_CERT_UPDATE}"
432+
self.charm.unit_peer_data[label_name] = json.dumps(waiting)

src/charm.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,11 @@ def unit_host(self, unit: Unit) -> str:
523523
else:
524524
raise ApplicationHostNotFoundError
525525

526+
@property
527+
def db_initialised(self) -> bool:
528+
"""Proxy for mongos_initialised, since some libs rely on db_initialised."""
529+
return self.mongos_initialised
530+
526531
@property
527532
def mongos_initialised(self) -> bool:
528533
"""Check if mongos is initialised."""

0 commit comments

Comments
 (0)