Skip to content

Commit eae45d2

Browse files
committed
mgr/cephadm: improving individual certificates checks
Some refactoring to improve the individual certificates checking and use the same code for both cases: certs with and without keys are Signed-off-by: Redouane Kachach <[email protected]>
1 parent dd6e81b commit eae45d2

File tree

5 files changed

+107
-62
lines changed

5 files changed

+107
-62
lines changed

src/pybind/mgr/cephadm/cert_mgr.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33

44
from cephadm.ssl_cert_utils import SSLCerts, SSLConfigException
5-
from mgr_util import verify_tls, ServerConfigException
5+
from mgr_util import verify_tls, verify_cacrt_content, ServerConfigException
66
from cephadm.ssl_cert_utils import get_certificate_info, get_private_key_info
77
from cephadm.tlsobject_types import Cert, PrivKey
88
from cephadm.tlsobject_store import TLSObjectStore, TLSObjectScope, TLSObjectException
@@ -328,7 +328,7 @@ def _notify_certificates_health_status(self, problematic_certificates: List[Cert
328328
logger.warning(short_error_msg)
329329
self.mgr.set_health_warning(CertMgr.CEPHADM_CERTMGR_HEALTH_ERR, short_error_msg, total_issues, detailed_error_msgs)
330330

331-
def check_certificate_state(self, cert_name: str, target: str, cert: str, key: str) -> CertInfo:
331+
def check_certificate_state(self, cert_name: str, target: str, cert: str, key: Optional[str] = None) -> CertInfo:
332332
"""
333333
Checks if a certificate is valid and close to expiration.
334334
@@ -339,17 +339,17 @@ def check_certificate_state(self, cert_name: str, target: str, cert: str, key: s
339339
- exception_info: Details of any exception encountered during validation.
340340
"""
341341
cert_obj = Cert(cert, True)
342-
key_obj = PrivKey(key, True)
342+
key_obj = PrivKey(key, True) if key else None
343343
return self._check_certificate_state(cert_name, target, cert_obj, key_obj)
344344

345-
def _check_certificate_state(self, cert_name: str, target: Optional[str], cert: Cert, key: PrivKey) -> CertInfo:
345+
def _check_certificate_state(self, cert_name: str, target: Optional[str], cert: Cert, key: Optional[PrivKey] = None) -> CertInfo:
346346
"""
347347
Checks if a certificate is valid and close to expiration.
348348
349349
Returns: CertInfo
350350
"""
351351
try:
352-
days_to_expiration = verify_tls(cert.cert, key.key)
352+
days_to_expiration = verify_tls(cert.cert, key.key) if key else verify_cacrt_content(cert.cert)
353353
is_close_to_expiration = days_to_expiration < self.mgr.certificate_renewal_threshold_days
354354
return CertInfo(cert_name, target, cert.user_made, True, is_close_to_expiration, days_to_expiration, "")
355355
except ServerConfigException as e:
@@ -393,9 +393,8 @@ def prepare_certificate(self,
393393

394394
def get_problematic_certificates(self) -> List[Tuple[CertInfo, Cert]]:
395395

396-
def get_key(cert_name: str, target: Optional[str]) -> Optional[PrivKey]:
396+
def get_key(cert_name: str, key_name: str, target: Optional[str]) -> Optional[PrivKey]:
397397
try:
398-
key_name = cert_name.replace('_cert', '_key')
399398
service_name, host = self.cert_store.determine_tlsobject_target(cert_name, target)
400399
key = cast(PrivKey, self.key_store.get_tlsobject(key_name, service_name=service_name, host=host))
401400
return key
@@ -407,21 +406,28 @@ def get_key(cert_name: str, target: Optional[str]) -> Optional[PrivKey]:
407406
problematics_certs: List[Tuple[CertInfo, Cert]] = []
408407
for cert_name, cert_tlsobj, target in certs_tlsobjs:
409408
cert_obj = cast(Cert, cert_tlsobj)
410-
key_obj = get_key(cert_name, target)
411-
if cert_obj and key_obj:
409+
if not cert_obj:
410+
logger.error(f'Cannot find certificate {cert_name} in the TLSObjectStore')
411+
continue
412+
413+
key_name = cert_name.replace('_cert', '_key')
414+
key_obj = get_key(cert_name, key_name, target)
415+
if key_obj:
416+
# certificate has a key, let's check the cert/key pair
412417
cert_info = self._check_certificate_state(cert_name, target, cert_obj, key_obj)
413-
if not cert_info.is_operationally_valid():
414-
problematics_certs.append((cert_info, cert_obj))
415-
else:
416-
target_info = f" ({target})" if target else ""
417-
logger.info(f'Certificate for "{cert_name}{target_info}" is still valid for {cert_info.days_to_expiration} days.')
418-
elif cert_obj:
419-
# Cert is present but key is None, could only happen if somebody has put manually a bad key!
420-
logger.warning(f"Key is missing for certificate '{cert_name}'.")
418+
elif key_name in self.known_keys:
419+
# certificate is supposed to have a key but it's missing
420+
logger.error(f"Key '{key_name}' is missing for certificate '{cert_name}'.")
421421
cert_info = CertInfo(cert_name, target, cert_obj.user_made, False, False, 0, "missing key")
422+
else:
423+
# certificate has no associated key
424+
cert_info = self._check_certificate_state(cert_name, target, cert_obj)
425+
426+
if not cert_info.is_operationally_valid():
422427
problematics_certs.append((cert_info, cert_obj))
423428
else:
424-
logger.error(f'Cannot get cert/key {cert_name}')
429+
target_info = f" ({target})" if target else ""
430+
logger.info(f'Certificate for "{cert_name}{target_info}" is still valid for {cert_info.days_to_expiration} days.')
425431

426432
return problematics_certs
427433

src/pybind/mgr/cephadm/module.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
NotifyType,
5959
MonCommandFailed,
6060
)
61-
from mgr_util import build_url, verify_cacrt_content, ServerConfigException
61+
from mgr_util import build_url
6262
import orchestrator
6363
from orchestrator.module import to_format, Format
6464

@@ -710,6 +710,7 @@ def _init_cert_mgr(self) -> None:
710710
self.cert_mgr.register_cert_key_pair('nvmeof', 'nvmeof_server_cert', 'nvmeof_server_key', TLSObjectScope.SERVICE)
711711
self.cert_mgr.register_cert_key_pair('nvmeof', 'nvmeof_client_cert', 'nvmeof_client_key', TLSObjectScope.SERVICE)
712712

713+
# register ancilary certificates/keys
713714
self.cert_mgr.register_cert('nvmeof', 'nvmeof_root_ca_cert', TLSObjectScope.SERVICE)
714715
self.cert_mgr.register_cert('rgw', 'rgw_frontend_ssl_cert', TLSObjectScope.SERVICE)
715716
self.cert_mgr.register_key('nvmeof', 'nvmeof_encryption_key', TLSObjectScope.SERVICE)
@@ -3291,18 +3292,16 @@ def cert_store_set_pair(
32913292
@handle_orch_error
32923293
def cert_store_set_cert(
32933294
self,
3294-
cert: str,
32953295
cert_name: str,
3296-
service_name: Optional[str] = None,
3297-
hostname: Optional[str] = None,
3296+
cert: str,
3297+
service_name: str = "",
3298+
hostname: str = "",
32983299
) -> str:
32993300

3300-
try:
3301-
days_to_expiration = verify_cacrt_content(cert)
3302-
if days_to_expiration < self.certificate_renewal_threshold_days:
3303-
raise OrchestratorError(f'Error: Certificate is about to expire (Remaining days: {days_to_expiration})')
3304-
except ServerConfigException as e:
3305-
raise OrchestratorError(f'Error: Invalid certificate for {cert_name}: {e}')
3301+
target = service_name or hostname
3302+
cert_info = self.cert_mgr.check_certificate_state(cert_name, target, cert)
3303+
if not cert_info.is_operationally_valid():
3304+
raise OrchestratorError(cert_info.get_status_description())
33063305

33073306
self.cert_mgr.save_cert(cert_name, cert, service_name, hostname, True)
33083307
return f'Certificate for {cert_name} set correctly'

src/pybind/mgr/cephadm/tests/test_certmgr.py

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -535,51 +535,91 @@ def test_tlsobject_store_key_ls(self, _set_store, cephadm_module: CephadmOrchest
535535
@mock.patch("cephadm.module.CephadmOrchestrator.get_store_prefix")
536536
def test_tlsobject_store_load(self, _get_store_prefix, cephadm_module: CephadmOrchestrator):
537537

538-
rgw_frontend_rgw_foo_host2_cert = 'fake-rgw-cert'
539-
grafana_host1_key = 'fake-grafana-host1-cert'
540-
nvmeof_server_cert = 'nvmeof-server-cert'
541-
nvmeof_client_cert = 'nvmeof-client-cert'
542-
nvmeof_root_ca_cert = 'nvmeof-root-ca-cert'
543-
nvmeof_server_key = 'nvmeof-server-key'
544-
nvmeof_client_key = 'nvmeof-client-key'
545-
nvmeof_encryption_key = 'nvmeof-encryption-key'
546-
unknown_cert_entity = 'unknown_per_service_cert'
547-
unknown_cert_key = 'unknown_per_service_key'
538+
# Define certs and keys with their corresponding scopes
539+
certs = {
540+
'rgw_frontend_ssl_cert': ('rgw.foo', 'fake-rgw-cert', TLSObjectScope.SERVICE),
541+
'nvmeof_server_cert': ('nvmeof.foo', 'nvmeof-server-cert', TLSObjectScope.SERVICE),
542+
'nvmeof_client_cert': ('nvmeof.foo', 'nvmeof-client-cert', TLSObjectScope.SERVICE),
543+
'nvmeof_root_ca_cert': ('nvmeof.foo', 'nvmeof-root-ca-cert', TLSObjectScope.SERVICE),
544+
'ingress_ssl_cert': ('ingress', 'ingress-ssl-cert', TLSObjectScope.SERVICE),
545+
'iscsi_ssl_cert': ('iscsi', 'iscsi-ssl-cert', TLSObjectScope.SERVICE),
546+
'grafana_cert': ('host1', 'grafana-cert', TLSObjectScope.HOST),
547+
'mgmt_gw_cert': ('mgmt-gateway', 'mgmt-gw-cert', TLSObjectScope.GLOBAL),
548+
'oauth2_proxy_cert': ('oauth2-proxy', 'oauth2-proxy-cert', TLSObjectScope.GLOBAL),
549+
}
550+
unknown_certs = {
551+
'unknown_per_service_cert': ('unknown-svc.foo', 'unknown-cert', TLSObjectScope.SERVICE),
552+
'unknown_per_host_cert': ('unknown-host.foo', 'unknown-cert', TLSObjectScope.HOST),
553+
'unknown_global_cert': ('unknown-global.foo', 'unknown-cert', TLSObjectScope.GLOBAL),
554+
'cert_with_unknown_scope': ('unknown-global.foo', 'unknown-cert', TLSObjectScope.UNKNOWN),
555+
}
548556

557+
keys = {
558+
'grafana_key': ('host1', 'fake-grafana-host1-key', TLSObjectScope.HOST),
559+
'nvmeof_server_key': ('nvmeof.foo', 'nvmeof-server-key', TLSObjectScope.SERVICE),
560+
'nvmeof_client_key': ('nvmeof.foo', 'nvmeof-client-key', TLSObjectScope.SERVICE),
561+
'nvmeof_encryption_key': ('nvmeof.foo', 'nvmeof-encryption-key', TLSObjectScope.SERVICE),
562+
'mgmt_gw_key': ('mgmt-gateway', 'mgmt-gw-key', TLSObjectScope.GLOBAL),
563+
'oauth2_proxy_key': ('oauth2-proxy', 'oauth2-proxy-key', TLSObjectScope.GLOBAL),
564+
'ingress_ssl_key': ('ingress', 'ingress-ssl-key', TLSObjectScope.SERVICE),
565+
'iscsi_ssl_key': ('iscsi', 'iscsi-ssl-key', TLSObjectScope.SERVICE),
566+
}
567+
unknown_keys = {
568+
'unknown_per_service_key': ('unknown-svc.foo', 'unknown-key', TLSObjectScope.SERVICE),
569+
'unknown_per_host_key': ('unknown-host.foo', 'unknown-key', TLSObjectScope.HOST),
570+
'unknown_global_key': ('unknown-global.foo', 'unknown-key', TLSObjectScope.GLOBAL),
571+
'key_with_unknown_scope': ('unknown-global.foo', 'unknown-key', TLSObjectScope.UNKNOWN),
572+
}
573+
574+
# Mock function to simulate store behavior
549575
def _fake_prefix_store(key):
576+
from itertools import chain
550577
if key == 'cert_store.cert.':
551578
return {
552-
f'{TLSOBJECT_STORE_CERT_PREFIX}rgw_frontend_ssl_cert': json.dumps({'rgw.foo': Cert(rgw_frontend_rgw_foo_host2_cert, True).to_json()}),
553-
f'{TLSOBJECT_STORE_CERT_PREFIX}nvmeof_server_cert': json.dumps({'nvmeof.foo': Cert(nvmeof_server_cert, True).to_json()}),
554-
f'{TLSOBJECT_STORE_CERT_PREFIX}nvmeof_client_cert': json.dumps({'nvmeof.foo': Cert(nvmeof_client_cert, True).to_json()}),
555-
f'{TLSOBJECT_STORE_CERT_PREFIX}nvmeof_root_ca_cert': json.dumps({'nvmeof.foo': Cert(nvmeof_root_ca_cert, True).to_json()}),
556-
f'{TLSOBJECT_STORE_CERT_PREFIX}{unknown_cert_entity}': json.dumps({'unkonwn.foo': Cert(rgw_frontend_rgw_foo_host2_cert, True).to_json()}),
579+
f'{TLSOBJECT_STORE_CERT_PREFIX}{cert_name}': json.dumps(
580+
{target: Cert(cert_value, True).to_json()} if scope != TLSObjectScope.GLOBAL
581+
else Cert(cert_value, True).to_json()
582+
)
583+
for cert_name, (target, cert_value, scope) in chain(certs.items(), unknown_certs.items())
557584
}
558585
elif key == 'cert_store.key.':
559586
return {
560-
f'{TLSOBJECT_STORE_KEY_PREFIX}grafana_key': json.dumps({'host1': PrivKey(grafana_host1_key).to_json()}),
561-
f'{TLSOBJECT_STORE_KEY_PREFIX}nvmeof_server_key': json.dumps({'nvmeof.foo': PrivKey(nvmeof_server_key).to_json()}),
562-
f'{TLSOBJECT_STORE_KEY_PREFIX}nvmeof_client_key': json.dumps({'nvmeof.foo': PrivKey(nvmeof_client_key).to_json()}),
563-
f'{TLSOBJECT_STORE_KEY_PREFIX}nvmeof_encryption_key': json.dumps({'nvmeof.foo': PrivKey(nvmeof_encryption_key).to_json()}),
564-
f'{TLSOBJECT_STORE_KEY_PREFIX}{unknown_cert_key}': json.dumps({'unkonwn.foo': PrivKey(nvmeof_encryption_key).to_json()}),
587+
f'{TLSOBJECT_STORE_KEY_PREFIX}{key_name}': json.dumps(
588+
{target: PrivKey(key_value).to_json()} if scope != TLSObjectScope.GLOBAL
589+
else PrivKey(key_value).to_json()
590+
)
591+
for key_name, (target, key_value, scope) in chain(keys.items(), unknown_keys.items())
565592
}
566593
else:
567-
raise Exception(f'Get store with unexpected value {key}')
594+
raise Exception(f'Unexpected key access in store: {key}')
568595

596+
# Inject the mock store behavior and the cert manager
569597
_get_store_prefix.side_effect = _fake_prefix_store
570598
cephadm_module._init_cert_mgr()
571599

572-
assert cephadm_module.cert_mgr.cert_store.known_entities['rgw_frontend_ssl_cert']['rgw.foo'] == Cert(rgw_frontend_rgw_foo_host2_cert, True)
573-
assert cephadm_module.cert_mgr.cert_store.known_entities['nvmeof_server_cert']['nvmeof.foo'] == Cert(nvmeof_server_cert, True)
574-
assert cephadm_module.cert_mgr.cert_store.known_entities['nvmeof_client_cert']['nvmeof.foo'] == Cert(nvmeof_client_cert, True)
575-
assert cephadm_module.cert_mgr.cert_store.known_entities['nvmeof_root_ca_cert']['nvmeof.foo'] == Cert(nvmeof_root_ca_cert, True)
576-
assert cephadm_module.cert_mgr.key_store.known_entities['grafana_key']['host1'] == PrivKey(grafana_host1_key)
577-
assert unknown_cert_entity not in cephadm_module.cert_mgr.cert_store.known_entities
578-
579-
assert cephadm_module.cert_mgr.key_store.known_entities['nvmeof_server_key']['nvmeof.foo'] == PrivKey(nvmeof_server_key)
580-
assert cephadm_module.cert_mgr.key_store.known_entities['nvmeof_client_key']['nvmeof.foo'] == PrivKey(nvmeof_client_key)
581-
assert cephadm_module.cert_mgr.key_store.known_entities['nvmeof_encryption_key']['nvmeof.foo'] == PrivKey(nvmeof_encryption_key)
582-
assert unknown_cert_key not in cephadm_module.cert_mgr.key_store.known_entities
600+
# Validate certificates in cert_store
601+
for cert_name, (target, cert_value, scope) in certs.items():
602+
assert cert_name in cephadm_module.cert_mgr.cert_store.known_entities
603+
if scope == TLSObjectScope.GLOBAL:
604+
assert cephadm_module.cert_mgr.cert_store.known_entities[cert_name] == Cert(cert_value, True)
605+
else:
606+
assert cephadm_module.cert_mgr.cert_store.known_entities[cert_name][target] == Cert(cert_value, True)
607+
608+
# Validate keys in key_store
609+
for key_name, (target, key_value, scope) in keys.items():
610+
assert key_name in cephadm_module.cert_mgr.key_store.known_entities
611+
if scope == TLSObjectScope.GLOBAL:
612+
assert cephadm_module.cert_mgr.key_store.known_entities[key_name] == PrivKey(key_value)
613+
else:
614+
assert cephadm_module.cert_mgr.key_store.known_entities[key_name][target] == PrivKey(key_value)
615+
616+
# Check unknown certificates are not loaded
617+
for unknown_cert in unknown_certs:
618+
assert unknown_cert not in cephadm_module.cert_mgr.cert_store.known_entities
619+
620+
# Check unknown keys are not loaded
621+
for unknown_key in unknown_keys:
622+
assert unknown_key not in cephadm_module.cert_mgr.key_store.known_entities
583623

584624
def test_tlsobject_store_get_cert_key(self, cephadm_module: CephadmOrchestrator):
585625

src/pybind/mgr/orchestrator/_interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,8 +607,8 @@ def cert_store_set_pair(
607607

608608
def cert_store_set_cert(
609609
self,
610-
cert: str,
611610
cert_name: str,
611+
cert: str,
612612
service_name: Optional[str] = None,
613613
hostname: Optional[str] = None,
614614
) -> OrchResult[str]:

src/pybind/mgr/orchestrator/module.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1287,8 +1287,8 @@ def _cert_store_set_cert(
12871287
raise OrchestratorError('This command requires passing a certificate using --cert parameter or "-i <filepath>" option')
12881288

12891289
completion = self.cert_store_set_cert(
1290-
cert_content,
12911290
cert_name,
1291+
cert_content,
12921292
service_name,
12931293
hostname,
12941294
)

0 commit comments

Comments
 (0)