Skip to content

Commit 0596664

Browse files
committed
mgr/cephadm: introducing new cmd to generate self-signed certs
this new Cephadm cmd introduces the ability to generate self-signed certificates for external modules, signed by Cephadm as the root CA. This feature is essential for implementing mTLS. Previously, if the user did not provide a certificate and key, the dashboard would generate its own. With this update, the dashboard now calls Cephadm to generate self-signed certificates, enabling secure mTLS communication with other backend applications. Prometheus module also makes use of this new functionality to generate self-signed certificates. Signed-off-by: Redouane Kachach <[email protected]>
1 parent 25a4f2a commit 0596664

File tree

8 files changed

+121
-65
lines changed

8 files changed

+121
-65
lines changed

src/cephadm/cephadm.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,11 +2421,23 @@ def prepare_dashboard(
24212421
pathify(ctx.dashboard_crt.name): '/tmp/dashboard.crt:z',
24222422
pathify(ctx.dashboard_key.name): '/tmp/dashboard.key:z'
24232423
}
2424-
cli(['dashboard', 'set-ssl-certificate', '-i', '/tmp/dashboard.crt'], extra_mounts=mounts)
2425-
cli(['dashboard', 'set-ssl-certificate-key', '-i', '/tmp/dashboard.key'], extra_mounts=mounts)
24262424
else:
2427-
logger.info('Generating a dashboard self-signed certificate...')
2428-
cli(['dashboard', 'create-self-signed-cert'])
2425+
logger.info('Using certmgr to generate dashboard self-signed certificate...')
2426+
cert_key = json_loads_retry(lambda: cli(['orch', 'certmgr', 'generate-certificates', 'dashboard'],
2427+
verbosity=CallVerbosity.QUIET_UNLESS_ERROR))
2428+
mounts = {}
2429+
if cert_key:
2430+
cert_file = write_tmp(cert_key['cert'], uid, gid)
2431+
key_file = write_tmp(cert_key['key'], uid, gid)
2432+
mounts = {
2433+
cert_file.name: '/tmp/dashboard.crt:z',
2434+
key_file.name: '/tmp/dashboard.key:z'
2435+
}
2436+
else:
2437+
logger.error('Cannot generate certificates for Ceph dashboard.')
2438+
2439+
cli(['dashboard', 'set-ssl-certificate', '-i', '/tmp/dashboard.crt'], extra_mounts=mounts)
2440+
cli(['dashboard', 'set-ssl-certificate-key', '-i', '/tmp/dashboard.key'], extra_mounts=mounts)
24292441

24302442
logger.info('Creating initial admin user...')
24312443
password = ctx.initial_dashboard_password or generate_password()

src/cephadm/tests/test_cephadm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ def wrap_test(address, expected):
282282
@mock.patch('cephadmlib.firewalld.Firewalld', mock_bad_firewalld)
283283
@mock.patch('cephadm.Firewalld', mock_bad_firewalld)
284284
@mock.patch('cephadm.logger')
285-
def test_skip_firewalld(self, _logger, cephadm_fs):
285+
@mock.patch('cephadm.json_loads_retry', return_value=None)
286+
def test_skip_firewalld(self, _logger, _jlr, cephadm_fs):
286287
"""
287288
test --skip-firewalld actually skips changing firewall
288289
"""

src/pybind/mgr/cephadm/module.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,6 @@ def __init__(self, *args: Any, **kwargs: Any):
539539
super(CephadmOrchestrator, self).__init__(*args, **kwargs)
540540
self._cluster_fsid: str = self.get('mon_map')['fsid']
541541
self.last_monmap: Optional[datetime.datetime] = None
542-
self.cert_mgr = CertMgr(self, self.get_mgr_ip())
543542

544543
# for serve()
545544
self.run = True
@@ -675,6 +674,8 @@ def __init__(self, *args: Any, **kwargs: Any):
675674
self.cert_key_store = CertKeyStore(self)
676675
self.cert_key_store.load()
677676

677+
self.cert_mgr = CertMgr(self, self.get_mgr_ip())
678+
678679
# ensure the host lists are in sync
679680
for h in self.inventory.keys():
680681
if h not in self.cache.daemons:
@@ -3085,6 +3086,14 @@ def _get_prometheus_credentials(self) -> Tuple[str, str]:
30853086
self.set_store(PrometheusService.PASS_CFG_KEY, password)
30863087
return (user, password)
30873088

3089+
@handle_orch_error
3090+
def generate_certificates(self, module_name: str) -> Optional[Dict[str, str]]:
3091+
supported_moduels = ['dashboard', 'prometheus']
3092+
if module_name not in supported_moduels:
3093+
raise OrchestratorError(f'Unsupported modlue {module_name}. Supported moduels are: {supported_moduels}')
3094+
cert, key = self.cert_mgr.generate_cert(self.get_hostname(), self.get_mgr_ip())
3095+
return {'cert': cert, 'key': key}
3096+
30883097
@handle_orch_error
30893098
def set_prometheus_access_info(self, user: str, password: str) -> str:
30903099
self.set_store(PrometheusService.USER_CFG_KEY, user)
@@ -3297,13 +3306,6 @@ def tuned_profile_rm_setting(self, profile_name: str, setting: str) -> str:
32973306
self._kick_serve_loop()
32983307
return f'Removed setting {setting} from tuned profile {profile_name}'
32993308

3300-
@handle_orch_error
3301-
def service_discovery_dump_cert(self) -> str:
3302-
root_cert = self.cert_key_store.get_cert('service_discovery_root_cert')
3303-
if not root_cert:
3304-
raise OrchestratorError('No certificate found for service discovery')
3305-
return root_cert
3306-
33073309
def set_health_warning(self, name: str, summary: str, count: int, detail: List[str]) -> None:
33083310
self.health_checks[name] = {
33093311
'severity': 'warning',

src/pybind/mgr/dashboard/controllers/prometheus.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,43 @@ def fetch_alert(self, **notification):
3232
class PrometheusRESTController(RESTController):
3333
def prometheus_proxy(self, method, path, params=None, payload=None):
3434
# type (str, str, dict, dict)
35-
user, password, cert_file = self.get_access_info('prometheus')
36-
verify = cert_file.name if cert_file else Settings.PROMETHEUS_API_SSL_VERIFY
35+
user, password, ca_cert_file, cert_file, key_file = self.get_access_info('prometheus')
36+
verify = ca_cert_file.name if ca_cert_file else Settings.PROMETHEUS_API_SSL_VERIFY
37+
cert = (cert_file.name, key_file.name) if cert_file and key_file else None
3738
response = self._proxy(self._get_api_url(Settings.PROMETHEUS_API_HOST),
3839
method, path, 'Prometheus', params, payload,
39-
user=user, password=password, verify=verify)
40-
if cert_file:
41-
cert_file.close()
42-
os.unlink(cert_file.name)
40+
user=user, password=password, verify=verify,
41+
cert=cert)
42+
for f in [ca_cert_file, cert_file, key_file]:
43+
if f:
44+
f.close()
45+
os.unlink(f.name)
4346
return response
4447

4548
def alert_proxy(self, method, path, params=None, payload=None):
4649
# type (str, str, dict, dict)
47-
user, password, cert_file = self.get_access_info('alertmanager')
48-
verify = cert_file.name if cert_file else Settings.ALERTMANAGER_API_SSL_VERIFY
50+
user, password, ca_cert_file, cert_file, key_file = self.get_access_info('alertmanager')
51+
verify = ca_cert_file.name if ca_cert_file else Settings.ALERTMANAGER_API_SSL_VERIFY
52+
cert = (cert_file.name, key_file.name) if cert_file and key_file else None
4953
response = self._proxy(self._get_api_url(Settings.ALERTMANAGER_API_HOST, version='v2'),
5054
method, path, 'Alertmanager', params, payload,
51-
user=user, password=password, verify=verify, is_alertmanager=True)
52-
if cert_file:
53-
cert_file.close()
54-
os.unlink(cert_file.name)
55+
user=user, password=password, verify=verify,
56+
cert=cert, is_alertmanager=True)
57+
for f in [ca_cert_file, cert_file, key_file]:
58+
if f:
59+
f.close()
60+
os.unlink(f.name)
5561
return response
5662

5763
def get_access_info(self, module_name):
58-
# type (str, str, str)
64+
# type (str, str, str, str, srt)
5965
if module_name not in ['prometheus', 'alertmanager']:
6066
raise DashboardException(f'Invalid module name {module_name}', component='prometheus')
6167
user = None
6268
password = None
6369
cert_file = None
70+
pkey_file = None
71+
ca_cert_file = None
6472

6573
orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator')
6674
if orch_backend == 'cephadm':
@@ -75,11 +83,25 @@ def get_access_info(self, module_name):
7583
user = access_info['user']
7684
password = access_info['password']
7785
certificate = access_info['certificate']
78-
cert_file = tempfile.NamedTemporaryFile(delete=False)
79-
cert_file.write(certificate.encode('utf-8'))
80-
cert_file.flush()
81-
82-
return user, password, cert_file
86+
ca_cert_file = tempfile.NamedTemporaryFile(delete=False)
87+
ca_cert_file.write(certificate.encode('utf-8'))
88+
ca_cert_file.flush()
89+
90+
cert_file = None
91+
cert = mgr.get_localized_store("crt") # type: ignore
92+
if cert is not None:
93+
cert_file = tempfile.NamedTemporaryFile(delete=False)
94+
cert_file.write(cert.encode('utf-8'))
95+
cert_file.flush() # cert_tmp must not be gc'ed
96+
97+
pkey_file = None
98+
pkey = mgr.get_localized_store("key") # type: ignore
99+
if pkey is not None:
100+
pkey_file = tempfile.NamedTemporaryFile(delete=False)
101+
pkey_file.write(pkey.encode('utf-8'))
102+
pkey_file.flush()
103+
104+
return user, password, ca_cert_file, cert_file, pkey_file
83105

84106
def _get_api_url(self, host, version='v1'):
85107
return f'{host.rstrip("/")}/api/{version}'
@@ -88,18 +110,19 @@ def balancer_status(self):
88110
return ceph_service.CephService.send_command('mon', 'balancer status')
89111

90112
def _proxy(self, base_url, method, path, api_name, params=None, payload=None, verify=True,
91-
user=None, password=None, is_alertmanager=False):
113+
user=None, password=None, is_alertmanager=False, cert=None):
92114
# type (str, str, str, str, dict, dict, bool)
93115
content = None
94116
try:
95117
from requests.auth import HTTPBasicAuth
96118
auth = HTTPBasicAuth(user, password) if user and password else None
97119
response = requests.request(method, base_url + path, params=params,
98120
json=payload, verify=verify,
121+
cert=cert,
99122
auth=auth)
100-
except Exception:
123+
except Exception as e:
101124
raise DashboardException(
102-
"Could not reach {}'s API on {}".format(api_name, base_url),
125+
"Could not reach {}'s API on {} error {}".format(api_name, base_url, e),
103126
http_status_code=404,
104127
component='prometheus')
105128
try:

src/pybind/mgr/dashboard/tests/test_prometheus.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def test_rules_cephadm(self, mock_request, mock_mon_command, mock_get_module_opt
3939
mock_request.assert_called_with('GET',
4040
self.prometheus_host_api + '/rules',
4141
json=None, params={},
42-
verify=True, auth=None)
42+
verify=True, cert=None, auth=None)
4343
assert mock_mon_command.called
4444

4545
@patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", return_value='cephadm')
@@ -55,45 +55,46 @@ def test_rules_rook(self, mock_request, mock_mon_command, mock_get_module_option
5555
self.prometheus_host_api + '/rules',
5656
json=None,
5757
params={},
58-
verify=True, auth=None)
58+
verify=True, cert=None, auth=None)
5959
assert not mock_mon_command.called
6060

6161
@patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
6262
def test_list(self):
6363
with patch('requests.request') as mock_request:
6464
self._get('/api/prometheus')
6565
mock_request.assert_called_with('GET', self.alert_host_api + '/alerts',
66-
json=None, params={}, verify=True, auth=None)
66+
json=None, params={}, verify=True, cert=None, auth=None)
6767

6868
@patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
6969
def test_get_silences(self):
7070
with patch('requests.request') as mock_request:
7171
self._get('/api/prometheus/silences')
7272
mock_request.assert_called_with('GET', self.alert_host_api + '/silences',
73-
json=None, params={}, verify=True, auth=None)
73+
json=None, params={}, verify=True, cert=None, auth=None)
7474

7575
@patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
7676
def test_add_silence(self):
7777
with patch('requests.request') as mock_request:
7878
self._post('/api/prometheus/silence', {'id': 'new-silence'})
7979
mock_request.assert_called_with('POST', self.alert_host_api + '/silences',
8080
params=None, json={'id': 'new-silence'},
81-
verify=True, auth=None)
81+
verify=True, cert=None, auth=None)
8282

8383
@patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
8484
def test_update_silence(self):
8585
with patch('requests.request') as mock_request:
8686
self._post('/api/prometheus/silence', {'id': 'update-silence'})
8787
mock_request.assert_called_with('POST', self.alert_host_api + '/silences',
8888
params=None, json={'id': 'update-silence'},
89-
verify=True, auth=None)
89+
verify=True, cert=None, auth=None)
9090

9191
@patch("dashboard.controllers.prometheus.mgr.get_module_option_ex", lambda a, b, c=None: None)
9292
def test_expire_silence(self):
9393
with patch('requests.request') as mock_request:
9494
self._delete('/api/prometheus/silence/0')
9595
mock_request.assert_called_with('DELETE', self.alert_host_api + '/silence/0',
96-
json=None, params=None, verify=True, auth=None)
96+
json=None, params=None, verify=True, cert=None,
97+
auth=None)
9798

9899
def test_silences_empty_delete(self):
99100
with patch('requests.request') as mock_request:

src/pybind/mgr/orchestrator/_interface.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,10 @@ def set_prometheus_access_info(self, user: str, password: str) -> OrchResult[str
793793
"""set prometheus access information"""
794794
raise NotImplementedError()
795795

796+
def generate_certificates(self, module_name: str) -> OrchResult[Optional[Dict[str, str]]]:
797+
"""set prometheus access information"""
798+
raise NotImplementedError()
799+
796800
def set_custom_prometheus_alerts(self, alerts_file: str) -> OrchResult[str]:
797801
"""set prometheus custom alerts files and schedule reconfig of prometheus"""
798802
raise NotImplementedError()

src/pybind/mgr/orchestrator/module.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,15 @@ def _get_credentials(self, username: Optional[str] = None, password: Optional[st
12071207

12081208
return _username, _password
12091209

1210+
@_cli_write_command('orch certmgr generate-certificates')
1211+
def _cert_mgr_generate_certificates(self, module_name: str) -> HandleCommandResult:
1212+
try:
1213+
completion = self.generate_certificates(module_name)
1214+
result = raise_if_exception(completion)
1215+
return HandleCommandResult(stdout=json.dumps(result))
1216+
except ArgumentError as e:
1217+
return HandleCommandResult(-errno.EINVAL, "", (str(e)))
1218+
12101219
@_cli_write_command('orch prometheus set-credentials')
12111220
def _set_prometheus_access_info(self, username: Optional[str] = None, password: Optional[str] = None, inbuf: Optional[str] = None) -> HandleCommandResult:
12121221
try:

src/pybind/mgr/prometheus/module.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
import enum
1111
from packaging import version # type: ignore
1212
from collections import namedtuple
13+
import tempfile
1314

1415
from mgr_module import CLIReadCommand, MgrModule, MgrStandbyModule, PG_STATES, Option, ServiceInfoT, HandleCommandResult, CLIWriteCommand
1516
from mgr_util import get_default_addr, profile_method, build_url
1617
from orchestrator import OrchestratorClientMixin, raise_if_exception, OrchestratorError
1718
from rbd import RBD
1819

19-
from typing import DefaultDict, Optional, Dict, Any, Set, cast, Tuple, Union, List, Callable
20+
from typing import DefaultDict, Optional, Dict, Any, Set, cast, Tuple, Union, List, Callable, IO
2021

2122
LabelValues = Tuple[str, ...]
2223
Number = Union[int, float]
@@ -616,6 +617,8 @@ class Module(MgrModule, OrchestratorClientMixin):
616617

617618
def __init__(self, *args: Any, **kwargs: Any) -> None:
618619
super(Module, self).__init__(*args, **kwargs)
620+
self.key_file: IO[bytes]
621+
self.cert_file: IO[bytes]
619622
self.metrics = self._setup_static_metrics()
620623
self.shutdown_event = threading.Event()
621624
self.collect_lock = threading.Lock()
@@ -1769,7 +1772,7 @@ def configure(self, server_addr: str, server_port: int) -> None:
17691772
'cephadm', 'secure_monitoring_stack', False)
17701773
if cephadm_secure_monitoring_stack:
17711774
try:
1772-
self.setup_cephadm_tls_config(server_addr, server_port)
1775+
self.setup_tls_config(server_addr, server_port)
17731776
return
17741777
except Exception as e:
17751778
self.log.exception(f'Failed to setup cephadm based secure monitoring stack: {e}\n',
@@ -1789,28 +1792,29 @@ def setup_default_config(self, server_addr: str, server_port: int) -> None:
17891792
self.set_uri(build_url(scheme='http', host=self.get_server_addr(),
17901793
port=server_port, path='/'))
17911794

1792-
def setup_cephadm_tls_config(self, server_addr: str, server_port: int) -> None:
1793-
from cephadm.ssl_cert_utils import SSLCerts
1794-
# the ssl certs utils uses a NamedTemporaryFile for the cert files
1795-
# generated with generate_cert_files function. We need the SSLCerts
1796-
# object to not be cleaned up in order to have those temp files not
1797-
# be cleaned up, so making it an attribute of the module instead
1798-
# of just a standalone object
1799-
self.cephadm_monitoring_tls_ssl_certs = SSLCerts()
1800-
host = self.get_mgr_ip()
1801-
try:
1802-
old_cert = self.get_store('root/cert')
1803-
old_key = self.get_store('root/key')
1804-
if not old_cert or not old_key:
1805-
raise Exception('No old credentials for mgr-prometheus endpoint')
1806-
self.cephadm_monitoring_tls_ssl_certs.load_root_credentials(old_cert, old_key)
1807-
except Exception:
1808-
self.cephadm_monitoring_tls_ssl_certs.generate_root_cert(host)
1809-
self.set_store('root/cert', self.cephadm_monitoring_tls_ssl_certs.get_root_cert())
1810-
self.set_store('root/key', self.cephadm_monitoring_tls_ssl_certs.get_root_key())
1811-
1812-
cert_file_path, key_file_path = self.cephadm_monitoring_tls_ssl_certs.generate_cert_files(
1813-
self.get_hostname(), host)
1795+
def setup_tls_config(self, server_addr: str, server_port: int) -> None:
1796+
from mgr_util import verify_tls_files
1797+
cmd = {'prefix': 'orch certmgr generate-certificates',
1798+
'module_name': 'prometheus',
1799+
'format': 'json'}
1800+
ret, out, err = self.mon_command(cmd)
1801+
if ret != 0:
1802+
self.log.error(f'mon command to generate-certificates failed: {err}')
1803+
return
1804+
elif out is None:
1805+
self.log.error('mon command to generate-certificates failed to generate certificates')
1806+
return
1807+
1808+
cert_key = json.loads(out)
1809+
self.cert_file = tempfile.NamedTemporaryFile()
1810+
self.cert_file.write(cert_key['cert'].encode('utf-8'))
1811+
self.cert_file.flush() # cert_tmp must not be gc'ed
1812+
self.key_file = tempfile.NamedTemporaryFile()
1813+
self.key_file.write(cert_key['key'].encode('utf-8'))
1814+
self.key_file.flush() # pkey_tmp must not be gc'ed
1815+
1816+
verify_tls_files(self.cert_file.name, self.key_file.name)
1817+
cert_file_path, key_file_path = self.cert_file.name, self.key_file.name
18141818

18151819
cherrypy.config.update({
18161820
'server.socket_host': server_addr,

0 commit comments

Comments
 (0)