Skip to content

Commit 3dbe2b6

Browse files
authored
Merge pull request ceph#58402 from rkachach/fix_issue_mtls_support
mgr/cephadm: adding mTLS for ceph mgmt-gateway and backend services communication Reviewed-by: Adam King <[email protected]>
2 parents 3f3d249 + d95e851 commit 3dbe2b6

36 files changed

+756
-577
lines changed

src/cephadm/cephadm.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
from cephadmlib.daemons.ceph import get_ceph_mounts_for_type, ceph_daemons
168168
from cephadmlib.daemons import (
169169
Ceph,
170+
CephExporter,
170171
CephIscsi,
171172
CephNvmeof,
172173
CustomContainer,
@@ -867,6 +868,10 @@ def create_daemon_dirs(
867868
node_proxy = NodeProxy.init(ctx, fsid, ident.daemon_id)
868869
node_proxy.create_daemon_dirs(data_dir, uid, gid)
869870

871+
elif daemon_type == CephExporter.daemon_type:
872+
ceph_exporter = CephExporter.init(ctx, fsid, ident.daemon_id)
873+
ceph_exporter.create_daemon_dirs(data_dir, uid, gid)
874+
870875
else:
871876
daemon = daemon_form_create(ctx, ident)
872877
if isinstance(daemon, ContainerDaemonForm):
@@ -2421,11 +2426,23 @@ def prepare_dashboard(
24212426
pathify(ctx.dashboard_crt.name): '/tmp/dashboard.crt:z',
24222427
pathify(ctx.dashboard_key.name): '/tmp/dashboard.key:z'
24232428
}
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)
24262429
else:
2427-
logger.info('Generating a dashboard self-signed certificate...')
2428-
cli(['dashboard', 'create-self-signed-cert'])
2430+
logger.info('Using certmgr to generate dashboard self-signed certificate...')
2431+
cert_key = json_loads_retry(lambda: cli(['orch', 'certmgr', 'generate-certificates', 'dashboard'],
2432+
verbosity=CallVerbosity.QUIET_UNLESS_ERROR))
2433+
mounts = {}
2434+
if cert_key:
2435+
cert_file = write_tmp(cert_key['cert'], uid, gid)
2436+
key_file = write_tmp(cert_key['key'], uid, gid)
2437+
mounts = {
2438+
cert_file.name: '/tmp/dashboard.crt:z',
2439+
key_file.name: '/tmp/dashboard.key:z'
2440+
}
2441+
else:
2442+
logger.error('Cannot generate certificates for Ceph dashboard.')
2443+
2444+
cli(['dashboard', 'set-ssl-certificate', '-i', '/tmp/dashboard.crt'], extra_mounts=mounts)
2445+
cli(['dashboard', 'set-ssl-certificate-key', '-i', '/tmp/dashboard.key'], extra_mounts=mounts)
24292446

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

src/cephadm/cephadmlib/daemons/ceph.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@
1616
from ..context import CephadmContext
1717
from ..deployment_utils import to_deployment_container
1818
from ..exceptions import Error
19-
from ..file_utils import make_run_dir, pathify
19+
from ..file_utils import (
20+
make_run_dir,
21+
pathify,
22+
populate_files,
23+
makedirs,
24+
recursive_chown,
25+
)
26+
from ..data_utils import dict_get
2027
from ..host_facts import HostFacts
2128
from ..logging import Highlight
2229
from ..net_utils import get_hostname, get_ip_addresses
@@ -298,6 +305,8 @@ def __init__(
298305
self.port = config_json.get('port', self.DEFAULT_PORT)
299306
self.prio_limit = config_json.get('prio-limit', 5)
300307
self.stats_period = config_json.get('stats-period', 5)
308+
self.https_enabled: bool = config_json.get('https_enabled', False)
309+
self.files = dict_get(config_json, 'files', {})
301310

302311
@classmethod
303312
def init(
@@ -323,6 +332,15 @@ def get_daemon_args(self) -> List[str]:
323332
f'--prio-limit={self.prio_limit}',
324333
f'--stats-period={self.stats_period}',
325334
]
335+
if self.https_enabled:
336+
args.extend(
337+
[
338+
'--cert-file',
339+
'/etc/certs/ceph-exporter.crt',
340+
'--key-file',
341+
'/etc/certs/ceph-exporter.key',
342+
]
343+
)
326344
return args
327345

328346
def validate(self) -> None:
@@ -348,6 +366,9 @@ def customize_container_mounts(
348366
) -> None:
349367
cm = Ceph.get_ceph_mounts(ctx, self.identity)
350368
mounts.update(cm)
369+
if self.https_enabled:
370+
data_dir = self.identity.data_dir(ctx.data_dir)
371+
mounts.update({os.path.join(data_dir, 'etc/certs'): '/etc/certs'})
351372

352373
def customize_process_args(
353374
self, ctx: CephadmContext, args: List[str]
@@ -376,6 +397,23 @@ def prepare_data_dir(self, data_dir: str, uid: int, gid: int) -> None:
376397
# it until now
377398
self.validate()
378399

400+
def create_daemon_dirs(self, data_dir: str, uid: int, gid: int) -> None:
401+
"""Create files under the container data dir"""
402+
if not os.path.isdir(data_dir):
403+
raise OSError('data_dir is not a directory: %s' % (data_dir))
404+
logger.info('Writing ceph-exporter config...')
405+
config_dir = os.path.join(data_dir, 'etc/')
406+
ssl_dir = os.path.join(data_dir, 'etc/certs')
407+
for ddir in [config_dir, ssl_dir]:
408+
makedirs(ddir, uid, gid, 0o755)
409+
recursive_chown(ddir, uid, gid)
410+
cert_files = {
411+
fname: content
412+
for fname, content in self.files.items()
413+
if fname.endswith('.crt') or fname.endswith('.key')
414+
}
415+
populate_files(ssl_dir, cert_files, uid, gid)
416+
379417

380418
def get_ceph_mounts_for_type(
381419
ctx: CephadmContext, fsid: str, daemon_type: str

src/cephadm/cephadmlib/daemons/mgmt_gateway.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,22 @@ def create_daemon_dirs(self, data_dir: str, uid: int, gid: int) -> None:
104104
raise OSError('data_dir is not a directory: %s' % (data_dir))
105105
logger.info('Writing mgmt-gateway config...')
106106
config_dir = os.path.join(data_dir, 'etc/')
107-
makedirs(config_dir, uid, gid, 0o755)
108-
recursive_chown(config_dir, uid, gid)
109-
populate_files(config_dir, self.files, uid, gid)
107+
ssl_dir = os.path.join(data_dir, 'etc/ssl')
108+
for ddir in [config_dir, ssl_dir]:
109+
makedirs(ddir, uid, gid, 0o755)
110+
recursive_chown(ddir, uid, gid)
111+
conf_files = {
112+
fname: content
113+
for fname, content in self.files.items()
114+
if fname.endswith('.conf')
115+
}
116+
cert_files = {
117+
fname: content
118+
for fname, content in self.files.items()
119+
if fname.endswith('.crt') or fname.endswith('.key')
120+
}
121+
populate_files(config_dir, conf_files, uid, gid)
122+
populate_files(ssl_dir, cert_files, uid, gid)
110123

111124
def _get_container_mounts(self, data_dir: str) -> Dict[str, str]:
112125
mounts: Dict[str, str] = {}
@@ -152,23 +165,6 @@ def customize_container_mounts(
152165
os.path.join(
153166
data_dir, 'etc/nginx_external_server.conf'
154167
): '/etc/nginx_external_server.conf:Z',
155-
os.path.join(
156-
data_dir, 'etc/nginx_internal.crt'
157-
): '/etc/nginx/ssl/nginx_internal.crt:Z',
158-
os.path.join(
159-
data_dir, 'etc/nginx_internal.key'
160-
): '/etc/nginx/ssl/nginx_internal.key:Z',
168+
os.path.join(data_dir, 'etc/ssl'): '/etc/nginx/ssl/',
161169
}
162170
)
163-
164-
if 'nginx.crt' in self.files:
165-
mounts.update(
166-
{
167-
os.path.join(
168-
data_dir, 'etc/nginx.crt'
169-
): '/etc/nginx/ssl/nginx.crt:Z',
170-
os.path.join(
171-
data_dir, 'etc/nginx.key'
172-
): '/etc/nginx/ssl/nginx.key:Z',
173-
}
174-
)

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/agent.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ class Server: # type: ignore
1010
import logging
1111
import socket
1212
import ssl
13-
import tempfile
1413
import threading
1514
import time
1615

@@ -20,11 +19,12 @@ class Server: # type: ignore
2019
from ceph.deployment.inventory import Devices
2120
from ceph.deployment.service_spec import ServiceSpec, PlacementSpec
2221
from cephadm.services.cephadmservice import CephadmDaemonDeploySpec
23-
from cephadm.ssl_cert_utils import SSLCerts
2422
from mgr_util import test_port_allocation, PortAlreadyInUse
23+
from mgr_util import verify_tls_files
24+
import tempfile
2525

2626
from urllib.error import HTTPError, URLError
27-
from typing import Any, Dict, List, Set, TYPE_CHECKING, Optional, MutableMapping
27+
from typing import Any, Dict, List, Set, TYPE_CHECKING, Optional, MutableMapping, IO
2828

2929
if TYPE_CHECKING:
3030
from cephadm.module import CephadmOrchestrator
@@ -46,9 +46,10 @@ class AgentEndpoint:
4646

4747
def __init__(self, mgr: "CephadmOrchestrator") -> None:
4848
self.mgr = mgr
49-
self.ssl_certs = SSLCerts()
5049
self.server_port = 7150
5150
self.server_addr = self.mgr.get_mgr_ip()
51+
self.key_file: IO[bytes]
52+
self.cert_file: IO[bytes]
5253

5354
def configure_routes(self) -> None:
5455
conf = {'/': {'tools.trailing_slash.on': False}}
@@ -57,19 +58,19 @@ def configure_routes(self) -> None:
5758
cherrypy.tree.mount(self.node_proxy_endpoint, '/node-proxy', config=conf)
5859

5960
def configure_tls(self, server: Server) -> None:
60-
old_cert = self.mgr.cert_key_store.get_cert('agent_endpoint_root_cert')
61-
old_key = self.mgr.cert_key_store.get_key('agent_endpoint_key')
61+
addr = self.mgr.get_mgr_ip()
62+
host = self.mgr.get_hostname()
63+
cert, key = self.mgr.cert_mgr.generate_cert(host, addr)
64+
self.cert_file = tempfile.NamedTemporaryFile()
65+
self.cert_file.write(cert.encode('utf-8'))
66+
self.cert_file.flush() # cert_tmp must not be gc'ed
6267

63-
if old_cert and old_key:
64-
self.ssl_certs.load_root_credentials(old_cert, old_key)
65-
else:
66-
self.ssl_certs.generate_root_cert(self.mgr.get_mgr_ip())
67-
self.mgr.cert_key_store.save_cert('agent_endpoint_root_cert', self.ssl_certs.get_root_cert())
68-
self.mgr.cert_key_store.save_key('agent_endpoint_key', self.ssl_certs.get_root_key())
68+
self.key_file = tempfile.NamedTemporaryFile()
69+
self.key_file.write(key.encode('utf-8'))
70+
self.key_file.flush() # pkey_tmp must not be gc'ed
6971

70-
host = self.mgr.get_hostname()
71-
addr = self.mgr.get_mgr_ip()
72-
server.ssl_certificate, server.ssl_private_key = self.ssl_certs.generate_cert_files(host, addr)
72+
verify_tls_files(self.cert_file.name, self.key_file.name)
73+
server.ssl_certificate, server.ssl_private_key = self.cert_file.name, self.key_file.name
7374

7475
def find_free_port(self) -> None:
7576
max_port = self.server_port + 150
@@ -94,7 +95,7 @@ def configure(self) -> None:
9495
class NodeProxyEndpoint:
9596
def __init__(self, mgr: "CephadmOrchestrator"):
9697
self.mgr = mgr
97-
self.ssl_root_crt = self.mgr.http_server.agent.ssl_certs.get_root_cert()
98+
self.ssl_root_crt = self.mgr.cert_mgr.get_root_ca()
9899
self.ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
99100
self.ssl_ctx.check_hostname = False
100101
self.ssl_ctx.verify_mode = ssl.CERT_NONE
@@ -301,7 +302,7 @@ def led(self, **kw: Any) -> Dict[str, Any]:
301302
endpoint: List[Any] = ['led', led_type]
302303
device: str = id_drive if id_drive else ''
303304

304-
ssl_root_crt = self.mgr.http_server.agent.ssl_certs.get_root_cert()
305+
ssl_root_crt = self.mgr.cert_mgr.get_root_ca()
305306
ssl_ctx = ssl.create_default_context()
306307
ssl_ctx.check_hostname = True
307308
ssl_ctx.verify_mode = ssl.CERT_REQUIRED
@@ -774,14 +775,13 @@ def run(self) -> None:
774775
self.mgr.agent_cache.sending_agent_message[self.host] = True
775776
try:
776777
assert self.agent
777-
root_cert = self.agent.ssl_certs.get_root_cert()
778+
root_cert = self.mgr.cert_mgr.get_root_ca()
778779
root_cert_tmp = tempfile.NamedTemporaryFile()
779780
root_cert_tmp.write(root_cert.encode('utf-8'))
780781
root_cert_tmp.flush()
781782
root_cert_fname = root_cert_tmp.name
782783

783-
cert, key = self.agent.ssl_certs.generate_cert(
784-
self.mgr.get_hostname(), self.mgr.get_mgr_ip())
784+
cert, key = self.mgr.cert_mgr.generate_cert(self.mgr.get_hostname(), self.mgr.get_mgr_ip())
785785

786786
cert_tmp = tempfile.NamedTemporaryFile()
787787
cert_tmp.write(cert.encode('utf-8'))
@@ -950,7 +950,7 @@ def _check_agent(self, host: str) -> bool:
950950
down = False
951951
try:
952952
assert self.agent
953-
assert self.agent.ssl_certs.get_root_cert()
953+
assert self.mgr.cert_mgr.get_root_ca()
954954
except Exception:
955955
self.mgr.log.debug(
956956
f'Delaying checking agent on {host} until cephadm endpoint finished creating root cert')
@@ -974,7 +974,7 @@ def _check_agent(self, host: str) -> bool:
974974
# so it's necessary to check this one specifically
975975
root_cert_match = False
976976
try:
977-
root_cert = self.agent.ssl_certs.get_root_cert()
977+
root_cert = self.mgr.cert_mgr.get_root_ca()
978978
if last_deps and root_cert in last_deps:
979979
root_cert_match = True
980980
except Exception:

src/pybind/mgr/cephadm/cert_mgr.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
from cephadm.ssl_cert_utils import SSLCerts, SSLConfigException
3+
from typing import TYPE_CHECKING, Tuple, Union, List
4+
5+
if TYPE_CHECKING:
6+
from cephadm.module import CephadmOrchestrator
7+
8+
9+
class CertMgr:
10+
11+
CEPHADM_ROOT_CA_CERT = 'cephadm_root_ca_cert'
12+
CEPHADM_ROOT_CA_KEY = 'cephadm_root_ca_key'
13+
14+
def __init__(self, mgr: "CephadmOrchestrator", ip: str) -> None:
15+
self.ssl_certs: SSLCerts = SSLCerts()
16+
old_cert = mgr.cert_key_store.get_cert(self.CEPHADM_ROOT_CA_CERT)
17+
old_key = mgr.cert_key_store.get_key(self.CEPHADM_ROOT_CA_KEY)
18+
if old_key and old_cert:
19+
try:
20+
self.ssl_certs.load_root_credentials(old_cert, old_key)
21+
except SSLConfigException:
22+
raise Exception("Cannot load cephadm root CA certificates.")
23+
else:
24+
self.ssl_certs.generate_root_cert(ip)
25+
mgr.cert_key_store.save_cert(self.CEPHADM_ROOT_CA_CERT, self.ssl_certs.get_root_cert())
26+
mgr.cert_key_store.save_key(self.CEPHADM_ROOT_CA_KEY, self.ssl_certs.get_root_key())
27+
28+
def get_root_ca(self) -> str:
29+
return self.ssl_certs.get_root_cert()
30+
31+
def generate_cert(self, host_fqdn: Union[str, List[str]], node_ip: Union[str, List[str]]) -> Tuple[str, str]:
32+
return self.ssl_certs.generate_cert(host_fqdn, node_ip)

src/pybind/mgr/cephadm/http_server.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def __init__(self, mgr: "CephadmOrchestrator") -> None:
3131
self.service_discovery = ServiceDiscovery(mgr)
3232
self.cherrypy_shutdown_event = threading.Event()
3333
self._service_discovery_port = self.mgr.service_discovery_port
34-
self.secure_monitoring_stack = self.mgr.secure_monitoring_stack
34+
security_enabled, mgmt_gw_enabled = self.mgr._get_security_config()
35+
self.security_enabled = security_enabled
3536
super().__init__(target=self.run)
3637

3738
def configure_cherrypy(self) -> None:
@@ -45,12 +46,13 @@ def configure(self) -> None:
4546
self.agent.configure()
4647
self.service_discovery.configure(self.mgr.service_discovery_port,
4748
self.mgr.get_mgr_ip(),
48-
self.secure_monitoring_stack)
49+
self.security_enabled)
4950

5051
def config_update(self) -> None:
5152
self.service_discovery_port = self.mgr.service_discovery_port
52-
if self.secure_monitoring_stack != self.mgr.secure_monitoring_stack:
53-
self.secure_monitoring_stack = self.mgr.secure_monitoring_stack
53+
security_enabled, mgmt_gw_enabled = self.mgr._get_security_config()
54+
if self.security_enabled != security_enabled:
55+
self.security_enabled = security_enabled
5456
self.restart()
5557

5658
@property

0 commit comments

Comments
 (0)