From f2a96c95efa5ea8a70f0d38b3098cbcaa057e31e Mon Sep 17 00:00:00 2001 From: Redouane Kachach Date: Thu, 15 Jan 2026 14:33:46 +0100 Subject: [PATCH] mgr/cephadm: adding new API to get nvmeof TLS bundle from certmgr MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This new API is intended for use by the Ceph Dashboard and keeps cephadm certmgr internals hidden from it. Certmgr populates the bundle based on the service’s configured certificate_source. https://tracker.ceph.com/issues/74377 Signed-off-by: Redouane Kachach --- src/pybind/mgr/cephadm/module.py | 7 ++ src/pybind/mgr/cephadm/services/nvmeof.py | 116 +++++++++++++++++++++- src/pybind/mgr/orchestrator/_interface.py | 3 + 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index 347305c64995..590763e8ae60 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -84,6 +84,7 @@ from .services.osd import OSDRemovalQueue, OSDService, OSD, NotFoundError from .services.monitoring import AlertmanagerService, PrometheusService from .services.node_proxy import NodeProxy from .services.smb import SMBService +from .services.nvmeof import NvmeofService from .schedule import HostAssignment from .inventory import ( Inventory, @@ -3488,6 +3489,12 @@ Then run the following: def cert_store_key_ls(self, include_cephadm_generated_keys: bool = False) -> Dict[str, Any]: return self.cert_mgr.key_ls(include_cephadm_generated_keys) + @handle_orch_error + def get_nvmeof_tls_bundle(self, service_name: str) -> Dict[str, str]: + nvmeof_svc = cast(NvmeofService, service_registry.get_service('nvmeof')) + tls_bundle = nvmeof_svc.get_nvmeof_tls_bundle(service_name) + return tls_bundle._asdict() if tls_bundle else {} + @handle_orch_error def cert_store_get_cert( self, diff --git a/src/pybind/mgr/cephadm/services/nvmeof.py b/src/pybind/mgr/cephadm/services/nvmeof.py index b811d0118f78..fa8ec1741434 100644 --- a/src/pybind/mgr/cephadm/services/nvmeof.py +++ b/src/pybind/mgr/cephadm/services/nvmeof.py @@ -1,7 +1,7 @@ import errno import logging import json -from typing import List, cast, Optional +from typing import List, cast, Optional, NamedTuple from ipaddress import ip_address, IPv6Address from mgr_module import HandleCommandResult @@ -21,6 +21,14 @@ logger = logging.getLogger(__name__) NVMEOF_CLIENT_CERT_LABEL = 'client' +class NvmeofTLSBundle(NamedTuple): + server_cert: str = '' + server_key: str = '' + client_cert: str = '' + client_key: str = '' + ca_cert: str = '' + + @register_cephadm_service class NvmeofService(CephService): TYPE = 'nvmeof' @@ -359,3 +367,109 @@ class NvmeofService(CephService): for blocking_daemon in blocking_daemons if blocking_daemon.hostname is not None ] return blocking_daemon_hosts + + def _pick_running_daemon_host_for_service(self, service_name: str) -> Optional[str]: + """ + Resolve a deterministic host for a service when HOST-scoped objects are needed. + Picks the first RUNNING daemon host from the orchestrator cache. + Returns None if none found. + """ + try: + dds = self.mgr.cache.get_daemons_by_service(service_name) + except Exception: + return None + + for dd in dds: + # dd.hostname is the short host name used for HOST-scoped certmgr objects + if dd.status == DaemonDescriptionStatus.running and dd.hostname: + return dd.hostname + + # fallback: any host if nothing is RUNNING + for dd in dds: + if dd.hostname: + return dd.hostname + + return None + + def get_nvmeof_tls_bundle(self, service_name: str) -> Optional[NvmeofTLSBundle]: + """ + Deterministic NVMeoF TLS bundle retrieval based on the NVMeoF spec's certificate_source. + + - INLINE: read from spec fields (ssl_cert/ssl_key[/client_cert/client_key/root_ca_cert]). + - REFERENCE: read from certmgr store objects (nvmeof_* names). + - CEPHADM_SIGNED: read from cephadm-signed objects (self_signed_* names) + certmgr root CA. + + """ + spec = cast(NvmeofServiceSpec, self.mgr.spec_store.all_specs.get(service_name, None)) + if spec is None: + return None + + # NVMeoF TLS may be disabled at spec level + if not getattr(spec, 'ssl', False): + return NvmeofTLSBundle() + + cert_source = getattr(spec, 'certificate_source', None) + valid_sources = [source.value for source in CertificateSource] + if cert_source not in valid_sources: + # Unknown / unset certificate_source + logger.error(f"Found unknown/invalid certificate_source='{cert_source}' for service '{service_name}'") + return None + + server_cert = '' + server_key = '' + client_cert = '' + client_key = '' + ca_cert = '' + cert_mgr = self.mgr.cert_mgr + enable_mtls = getattr(spec, 'enable_auth', False) + + # -------- INLINE -------- + if cert_source == CertificateSource.INLINE.value: + server_cert = getattr(spec, 'server_cert', None) or getattr(spec, 'ssl_cert', '') or '' + server_key = getattr(spec, 'server_key', None) or getattr(spec, 'ssl_key', '') or '' + ca_cert = getattr(spec, 'root_ca_cert', '') or '' + if enable_mtls: + client_cert = getattr(spec, 'client_cert', '') or '' + client_key = getattr(spec, 'client_key', '') or '' + + # -------- REFERENCE -------- + elif cert_source == CertificateSource.REFERENCE.value: + server_cert = cert_mgr.get_cert(self.cert_name, service_name=service_name) or '' + server_key = cert_mgr.get_key(self.key_name, service_name=service_name) or '' + ca_cert = cert_mgr.get_cert(self.ca_cert_name, service_name=service_name) or '' + if enable_mtls: + client_cert = cert_mgr.get_cert(self.client_cert_name, service_name=service_name) or '' + client_key = cert_mgr.get_key(self.client_key_name, service_name=service_name) or '' + + # -------- CEPHADM_SIGNED -------- + elif cert_source == CertificateSource.CEPHADM_SIGNED.value: + hostname = self._pick_running_daemon_host_for_service(service_name) + if not hostname: + logger.error(f"certificate_source=cephadm-signed for '{service_name}' but no hostname could be resolved") + return None + + server_creds = cert_mgr.get_self_signed_tls_credentials(service_name, hostname) + server_cert = server_creds.cert + server_key = server_creds.key + ca_cert = server_creds.ca_cert or '' + if enable_mtls: + client_creds = cert_mgr.get_self_signed_tls_credentials(service_name, hostname, NVMEOF_CLIENT_CERT_LABEL) + client_cert = client_creds.cert + client_key = client_creds.key + + # -------- Build bundle -------- + # Return bundle with client creds only if mTLS is enabled + if enable_mtls: + return NvmeofTLSBundle( + client_cert=client_cert, + client_key=client_key, + server_cert=server_cert, + server_key=server_key, + ca_cert=ca_cert + ) + else: + return NvmeofTLSBundle( + server_cert=server_cert, + server_key=server_key, + ca_cert=ca_cert + ) diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index 136fde595ac0..14f108dfc7fc 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -539,6 +539,9 @@ class Orchestrator(object): def cert_store_key_ls(self, include_cephadm_generated_keys: bool = False) -> OrchResult[Dict[str, Any]]: raise NotImplementedError() + def get_nvmeof_tls_bundle(self, service_name: str) -> OrchResult[Dict[str, str]]: + raise NotImplementedError() + def cert_store_get_cert( self, cert_name: str, -- 2.47.3