From: Shweta Bhosale Date: Tue, 23 Sep 2025 15:50:04 +0000 (+0530) Subject: mgr/cephadm: Cephadm support for NFS-Ganesha TLS configuration X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=3838790ad1184a5691f9b94de0033c56bd276e62;p=ceph-ci.git mgr/cephadm: Cephadm support for NFS-Ganesha TLS configuration Fixes: https://tracker.ceph.com/issues/73035 Signed-off-by: Shweta Bhosale Resolves: rhbz#2395169 Conflicts: src/cephadm/cephadmlib/daemons/nfs.py src/pybind/mgr/cephadm/serve.py src/pybind/mgr/cephadm/services/nfs.py src/pybind/mgr/nfs/cluster.py src/pybind/mgr/nfs/module.py src/python-common/ceph/deployment/service_spec.py --- diff --git a/doc/cephadm/services/nfs.rst b/doc/cephadm/services/nfs.rst index 110c8369339..f7b2caabc37 100644 --- a/doc/cephadm/services/nfs.rst +++ b/doc/cephadm/services/nfs.rst @@ -79,6 +79,85 @@ address is not present and ``monitoring_networks`` is specified, an IP address that matches one of the specified networks will be used. If neither condition is met, the default binding will happen on all available network interfaces. +TLS/SSL Example +--------------- + +Here's an example NFS service specification with TLS/SSL configuration: + +.. code-block:: yaml + + service_type: nfs + service_id: mynfs + placement: + hosts: + - ceph-node-0 + spec: + port: 12345 + ssl: true + certificate_source: inline|reference|cephadm-signed + ssl_cert: | + -----BEGIN CERTIFICATE----- + (PEM cert contents here) + -----END CERTIFICATE----- + ssl_key: | + -----BEGIN PRIVATE KEY----- + (PEM key contents here) + -----END PRIVATE KEY----- + ssl_ca_cert: + -----BEGIN PRIVATE KEY----- + (PEM key contents here) + -----END PRIVATE KEY----- + tls_ktls: true + tls_debug: true + tls_min_version: TLSv1.3 + tls_ciphers: AES-256 + +This example configures an NFS service with TLS encryption enabled using +inline certificates. + +TLS/SSL Parameters +~~~~~~~~~~~~~~~~~~ + +The following parameters can be used to configure TLS/SSL encryption for the NFS service: + +* ``ssl`` (boolean): Enable or disable SSL/TLS encryption. Default is ``false``. + +* ``certificate_source`` (string): Specifies the source of the TLS certificates. + Options include: + + - ``cephadm-signed``: Use certificates signed by cephadm's internal CA + - ``inline``: Provide certificates directly in the specification using ``ssl_cert``, ``ssl_key``, and ``ssl_ca_cert`` fields + - ``reference``: Users can register their own certificate and key with certmgr and + set the ``certificate_source`` to ``reference`` in the spec. + +* ``ssl_cert`` (string): The SSL certificate in PEM format. Required when using + ``inline`` certificate source. + +* ``ssl_key`` (string): The SSL private key in PEM format. Required when using + ``inline`` certificate source. + +* ``ssl_ca_cert`` (string): The SSL CA certificate in PEM format. Required when + using ``inline`` certificate source. + +* ``custom_sans`` (list): List of custom Subject Alternative Names (SANs) to + include in the certificate. + +* ``tls_ktls`` (boolean): Enable kernel TLS (kTLS) for improved performance when + available. Default is ``false``. + +* ``tls_debug`` (boolean): Enable TLS debugging output. Useful for troubleshooting + TLS issues. Default is ``false``. + +* ``tls_min_version`` (string): Specify the minimum TLS version to accept. + Examples: TLSv1.3, TLSv1.2 + +* ``tls_ciphers`` (string): Specify allowed cipher suites for TLS connections. + Example: :-CIPHER-ALL:+AES-256-GCM + +.. note:: When ``ssl`` is enabled, a ``certificate_source`` must be specified. + If using ``inline`` certificates, all three certificate fields (``ssl_cert``, + ``ssl_key``, ``ssl_ca_cert``) must be provided. + The specification can then be applied by running the following command: .. prompt:: bash # diff --git a/doc/mgr/nfs.rst b/doc/mgr/nfs.rst index 3a130f4f0a9..485200e1f5c 100644 --- a/doc/mgr/nfs.rst +++ b/doc/mgr/nfs.rst @@ -182,7 +182,7 @@ In order to modify cluster parameters (for example, the port or the placement), use the orchestrator interface to update the NFS service spec. The safest way to do that is to export the current spec, modify it, and then re-apply it. For example, to modify the ``nfs.foo`` service, run commands of the following -forms: +forms: .. prompt:: bash # @@ -318,7 +318,7 @@ value is ``no_root_squash``. See the `NFS-Ganesha Export Sample`_ for permissible values. ```` specifies which authentication methods will be used when -connecting to the export. Valid values include "krb5p", "krb5i", "krb5", "sys", +connecting to the export. Valid values include "krb5p", "krb5i", "krb5", "sys", "tls", "mtls" and "none". More than one value can be supplied. The flag may be specified multiple times (example: ``--sectype=krb5p --sectype=krb5i``) or multiple values may be separated by a comma (example: ``--sectype krb5p,krb5i``). The @@ -350,7 +350,7 @@ There are two kinds of RGW exports: RGW bucket export ^^^^^^^^^^^^^^^^^ - + To export a *bucket*: .. prompt:: bash # diff --git a/src/cephadm/cephadmlib/daemons/nfs.py b/src/cephadm/cephadmlib/daemons/nfs.py index 19751d3df21..9be805a9d16 100644 --- a/src/cephadm/cephadmlib/daemons/nfs.py +++ b/src/cephadm/cephadmlib/daemons/nfs.py @@ -180,19 +180,30 @@ class NFSGanesha(ContainerDaemonForm): makedirs(config_dir, uid, gid, 0o755) makedirs(kmip_dir, uid, gid, 0o755) + tls_dir = os.path.join(data_dir, 'etc/ganesha/tls') + makedirs(config_dir, uid, gid, 0o755) + makedirs(tls_dir, uid, gid, 0o755) + config_files = { fname: content for fname, content in self.files.items() - if fname.endswith('.conf') + if fname in ['ganesha.conf', 'idmap.conf'] } kmip_files = { fname: content for fname, content in self.files.items() if fname.startswith('kmip') } + tls_files = { + fname: content + for fname, content in self.files.items() + if fname.startswith('tls') + } + # populate files from the config-json populate_files(config_dir, config_files, uid, gid) populate_files(kmip_dir, kmip_files, uid, gid) + populate_files(tls_dir, tls_files, uid, gid) # write the RGW keyring if self.rgw: diff --git a/src/pybind/mgr/cephadm/services/ingress.py b/src/pybind/mgr/cephadm/services/ingress.py index 35829f1ad7b..ecfafd8a939 100644 --- a/src/pybind/mgr/cephadm/services/ingress.py +++ b/src/pybind/mgr/cephadm/services/ingress.py @@ -305,7 +305,7 @@ class IngressService(CephService): if spec.monitor_ssl and spec.monitor_cert_source != MonitorCertSource.REUSE_SERVICE_CERT.value: tls_creds = self.get_stats_certs(spec, daemon_spec, monitor_ips) monitor_ssl_cert = [tls_creds.cert, tls_creds.key] - config_files['files']['stats_haproxy.pem'] = '\n'.join(monitor_ssl_cert) + final_config['files']['stats_haproxy.pem'] = '\n'.join(monitor_ssl_cert) return final_config, self.get_haproxy_dependencies(self.mgr, spec) diff --git a/src/pybind/mgr/cephadm/services/nfs.py b/src/pybind/mgr/cephadm/services/nfs.py index 04aa4be73f3..4226109db29 100644 --- a/src/pybind/mgr/cephadm/services/nfs.py +++ b/src/pybind/mgr/cephadm/services/nfs.py @@ -116,11 +116,21 @@ class NFSService(CephService): deps.append(f'kmip_key: {str(utils.md5_hash(nfs_spec.kmip_key))}') deps.append(f'kmip_ca_cert: {str(utils.md5_hash(nfs_spec.kmip_ca_cert))}') deps.append(f'kmip_host_list: {nfs_spec.kmip_host_list}') + # TLS related fields + if (spec.ssl and spec.ssl_cert and spec.ssl_key and spec.ssl_ca_cert): + deps.append(f'ssl_cert: {str(utils.md5_hash(spec.ssl_cert))}') + deps.append(f'ssl_key: {str(utils.md5_hash(spec.ssl_key))}') + deps.append(f'ssl_ca_cert: {str(utils.md5_hash(spec.ssl_ca_cert))}') + deps.append(f'tls_ktls: {nfs_spec.tls_ktls}') + deps.append(f'tls_debug: {nfs_spec.tls_debug}') + deps.append(f'tls_min_version: {nfs_spec.tls_min_version}') + deps.append(f'tls_ciphers: {nfs_spec.tls_ciphers}') return sorted(deps) def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]: assert self.TYPE == daemon_spec.daemon_type + super().register_for_certificates(daemon_spec) daemon_type = daemon_spec.daemon_type daemon_id = daemon_spec.daemon_id host = daemon_spec.host @@ -184,7 +194,12 @@ class NFSService(CephService): "cluster_id": self.mgr._cluster_fsid, "enable_virtual_server": str(spec.enable_virtual_server).lower(), "kmip_addrs": spec.kmip_host_list if add_kmip_block else None, - "use_old_nodeid": False if nodeid.isdigit() else True + "use_old_nodeid": False if nodeid.isdigit() else True, + "tls_add": spec.ssl, + "tls_ciphers": spec.tls_ciphers, + "tls_min_version": spec.tls_min_version, + "tls_ktls": spec.tls_ktls, + "tls_debug": spec.tls_debug, } if spec.enable_haproxy_protocol: context["haproxy_hosts"] = self._haproxy_hosts() @@ -225,6 +240,14 @@ class NFSService(CephService): 'kmip_ca_cert', ]: config['files'][f'{kmip_cert_key_field}.pem'] = getattr(spec, kmip_cert_key_field) + + if spec.ssl: + tls_creds = self.get_certificates(daemon_spec, ca_cert_required=True) + config['files'].update({ + 'tls_cert.pem': tls_creds.cert, + 'tls_key.pem': tls_creds.key, + 'tls_ca_cert.pem': tls_creds.ca_cert, + }) config.update( self.get_config_and_keyring( daemon_type, daemon_id, diff --git a/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 b/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 index bd535054ef8..35b0f6a0e45 100644 --- a/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 +++ b/src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2 @@ -81,4 +81,25 @@ KMIP { } {% endif %} +{% if tls_add %} +TLS_CONFIG{ + Enable_TLS = {{ tls_add }}; + TLS_Cert_File = /etc/ganesha/tls/tls_cert.pem; + TLS_Key_File = /etc/ganesha/tls/tls_key.pem; + TLS_CA_File = /etc/ganesha/tls/tls_ca_cert.pem; + {% if tls_ciphers %} + TLS_Ciphers = "{{ tls_ciphers }}"; + {% endif %} + {% if tls_min_version %} + TLS_Min_Version = "{{ tls_min_version }}"; + {% endif %} + {% if tls_ktls %} + Enable_KTLS = {{ tls_ktls }}; + {% endif %} + {% if tls_debug %} + Enable_debug = {{ tls_debug }}; + {% endif %} +} + +{% endif %} %url {{ url }} diff --git a/src/pybind/mgr/cephadm/tests/test_certmgr.py b/src/pybind/mgr/cephadm/tests/test_certmgr.py index b6b550456a4..0aa39a04de5 100644 --- a/src/pybind/mgr/cephadm/tests/test_certmgr.py +++ b/src/pybind/mgr/cephadm/tests/test_certmgr.py @@ -305,12 +305,16 @@ class TestCertMgr(object): nvmeof_root_ca_cert = 'fake-nvmeof-root-ca-cert' grafana_cert_host_1 = 'grafana-cert-host-1' grafana_cert_host_2 = 'grafana-cert-host-2' + nfs_ssl_cert = 'nfs-ssl-cert' + nfs_ssl_ca_cert = 'nfs-ssl-ca-cert' cephadm_module.cert_mgr.save_cert('rgw_ssl_cert', rgw_frontend_rgw_foo_host2_cert, service_name='rgw.foo', user_made=True) cephadm_module.cert_mgr.save_cert('nvmeof_ssl_cert', nvmeof_ssl_cert, service_name='nvmeof.self-signed.foo', user_made=False) cephadm_module.cert_mgr.save_cert('nvmeof_client_cert', nvmeof_client_cert, service_name='nvmeof.foo', user_made=True) cephadm_module.cert_mgr.save_cert('nvmeof_root_ca_cert', nvmeof_root_ca_cert, service_name='nvmeof.foo', user_made=True) cephadm_module.cert_mgr.save_cert('grafana_ssl_cert', grafana_cert_host_1, host='host-1', user_made=True) cephadm_module.cert_mgr.save_cert('grafana_ssl_cert', grafana_cert_host_2, host='host-2', user_made=True) + cephadm_module.cert_mgr.save_cert('nfs_ssl_cert', nfs_ssl_cert, service_name='nfs.foo', user_made=True) + cephadm_module.cert_mgr.save_cert('nfs_ssl_ca_cert', nfs_ssl_ca_cert, service_name='nfs.foo', user_made=True) expected_calls = [ mock.call(f'{TLSOBJECT_STORE_CERT_PREFIX}rgw_ssl_cert', json.dumps({'rgw.foo': Cert(rgw_frontend_rgw_foo_host2_cert, True).to_json()})), @@ -319,7 +323,9 @@ class TestCertMgr(object): mock.call(f'{TLSOBJECT_STORE_CERT_PREFIX}nvmeof_root_ca_cert', json.dumps({'nvmeof.foo': Cert(nvmeof_root_ca_cert, True).to_json()})), mock.call(f'{TLSOBJECT_STORE_CERT_PREFIX}grafana_ssl_cert', json.dumps({'host-1': Cert(grafana_cert_host_1, True).to_json()})), mock.call(f'{TLSOBJECT_STORE_CERT_PREFIX}grafana_ssl_cert', json.dumps({'host-1': Cert(grafana_cert_host_1, True).to_json(), - 'host-2': Cert(grafana_cert_host_2, True).to_json()})) + 'host-2': Cert(grafana_cert_host_2, True).to_json()})), + mock.call(f'{TLSOBJECT_STORE_CERT_PREFIX}nfs_ssl_cert', json.dumps({'nfs.foo': Cert(nfs_ssl_cert, True).to_json()})), + mock.call(f'{TLSOBJECT_STORE_CERT_PREFIX}nfs_ssl_ca_cert', json.dumps({'nfs.foo': Cert(nfs_ssl_ca_cert, True).to_json()})), ] _set_store.assert_has_calls(expected_calls) @@ -424,6 +430,24 @@ class TestCertMgr(object): } compare_certls_dicts(expected_ls) + cephadm_module.cert_mgr.save_cert('nfs_ssl_cert', CEPHADM_SELF_GENERATED_CERT_1, service_name='nfs.foo', user_made=True) + expected_ls["nfs_ssl_cert"] = { + "scope": "service", + "certificates": { + "nfs.foo": get_generated_cephadm_cert_info_1(), + }, + } + compare_certls_dicts(expected_ls) + + cephadm_module.cert_mgr.save_cert('nfs_ssl_ca_cert', CEPHADM_SELF_GENERATED_CERT_2, service_name='nfs.foo', user_made=True) + expected_ls["nfs_ssl_ca_cert"] = { + "scope": "service", + "certificates": { + "nfs.foo": get_generated_cephadm_cert_info_2(), + }, + } + compare_certls_dicts(expected_ls) + # Services with host target/scope cephadm_module.cert_mgr.save_cert('grafana_ssl_cert', CEPHADM_SELF_GENERATED_CERT_1, host='host1', user_made=True) cephadm_module.cert_mgr.save_cert('grafana_ssl_cert', CEPHADM_SELF_GENERATED_CERT_2, host='host2', user_made=True) @@ -586,6 +610,8 @@ class TestCertMgr(object): 'grafana_ssl_cert': ('host1', 'grafana-cert', TLSObjectScope.HOST), 'oauth2_proxy_ssl_cert': ('host1', 'oauth2-proxy', TLSObjectScope.HOST), 'mgmt_gateway_ssl_cert': ('mgmt-gateway', 'mgmt-gw-cert', TLSObjectScope.GLOBAL), + 'nfs_ssl_cert': ('nfs.foo', 'nfs-ssl-cert', TLSObjectScope.SERVICE), + 'nfs_ssl_ca_cert': ('nfs.foo', 'nfs-ssl-ca-cert', TLSObjectScope.SERVICE), } unknown_certs = { 'unknown_per_service_cert': ('unknown-svc.foo', 'unknown-cert', TLSObjectScope.SERVICE), @@ -602,6 +628,7 @@ class TestCertMgr(object): 'oauth2_proxy_ssl_key': ('host1', 'oauth2-proxy', TLSObjectScope.HOST), 'ingress_ssl_key': ('ingress', 'ingress-ssl-key', TLSObjectScope.SERVICE), 'iscsi_ssl_key': ('iscsi', 'iscsi-ssl-key', TLSObjectScope.SERVICE), + 'nfs_ssl_key': ('nfs.foo', 'nfs-ssl-key', TLSObjectScope.SERVICE), } unknown_keys = { 'unknown_per_service_key': ('unknown-svc.foo', 'unknown-key', TLSObjectScope.SERVICE), @@ -674,9 +701,12 @@ class TestCertMgr(object): good_certs = { 'rgw_ssl_cert': ('rgw.foo', 'good-cert', TLSObjectScope.SERVICE), 'mgmt_gateway_ssl_cert': ('mgmt-gateway', 'good-global-cert', TLSObjectScope.GLOBAL), + 'nfs_ssl_cert': ('nfs.foo', 'nfs-ssl-cert', TLSObjectScope.SERVICE), + 'nfs_ssl_ca_cert': ('nfs.foo', 'nfs-ssl-ca-cert', TLSObjectScope.SERVICE), } good_keys = { 'rgw_ssl_key': ('rgw.foo', 'good-key', TLSObjectScope.SERVICE), + 'nfs_ssl_key': ('nfs.foo', 'nfs-ssl-key', TLSObjectScope.SERVICE), } # Helpers to dump valid JSON structures @@ -723,10 +753,16 @@ class TestCertMgr(object): # Good entries loaded correctly assert 'rgw_ssl_cert' in cert_store assert cert_store['rgw_ssl_cert']['rgw.foo'] == Cert('good-cert', True) + assert 'nfs_ssl_cert' in cert_store + assert cert_store['nfs_ssl_cert']['nfs.foo'] == Cert('nfs-ssl-cert', True) + assert 'nfs_ssl_ca_cert' in cert_store + assert cert_store['nfs_ssl_ca_cert']['nfs.foo'] == Cert('nfs-ssl-ca-cert', True) assert 'mgmt_gateway_ssl_cert' in cert_store assert cert_store['mgmt_gateway_ssl_cert'] == Cert('good-global-cert', True) assert 'rgw_ssl_key' in key_store assert key_store['rgw_ssl_key']['rgw.foo'] == PrivKey('good-key') + assert 'nfs_ssl_key' in key_store + assert key_store['nfs_ssl_key']['nfs.foo'] == PrivKey('nfs-ssl-key') # Bad ones: object names exist (pre-registered), but **no targets** were added # Service / Host scoped => dict should be empty diff --git a/src/pybind/mgr/cephadm/tests/test_services.py b/src/pybind/mgr/cephadm/tests/test_services.py index 03dd968009d..39829c1b1fc 100644 --- a/src/pybind/mgr/cephadm/tests/test_services.py +++ b/src/pybind/mgr/cephadm/tests/test_services.py @@ -4448,6 +4448,44 @@ class TestNFS: assert gen_config_lines == exp_config_lines + @patch("cephadm.serve.CephadmServe._run_cephadm") + @patch("cephadm.services.nfs.NFSService.fence_old_ranks", MagicMock()) + @patch("cephadm.services.nfs.NFSService.run_grace_tool", MagicMock()) + @patch("cephadm.services.nfs.NFSService.purge", MagicMock()) + @patch("cephadm.services.nfs.NFSService.create_rados_config_obj", MagicMock()) + def test_nfs_tls(self, _run_cephadm, cephadm_module: CephadmOrchestrator): + _run_cephadm.side_effect = async_side_effect(('{}', '', 0)) + + with with_host(cephadm_module, 'test', addr='1.2.3.7'): + cephadm_module.cache.update_host_networks('test', { + '1.2.3.0/24': { + 'if0': ['1.2.3.1'] + } + }) + + nfs_spec = NFSServiceSpec(service_id="foo", placement=PlacementSpec(hosts=['test']), + ssl=True, ssl_cert=ceph_generated_cert, ssl_key=ceph_generated_key, + ssl_ca_cert=cephadm_root_ca, certificate_source='inline', tls_ktls=True, + tls_debug=True, tls_min_version='TLSv1.3', + tls_ciphers='ECDHE-ECDSA-AES256') + with with_service(cephadm_module, nfs_spec) as _: + nfs_generated_conf, _ = service_registry.get_service('nfs').generate_config( + CephadmDaemonDeploySpec(host='test', daemon_id='foo.test.0.0', service_name=nfs_spec.service_name())) + ganesha_conf = nfs_generated_conf['files']['ganesha.conf'] + expected_tls_block = ( + 'TLS_CONFIG{\n' + ' Enable_TLS = True;\n' + ' TLS_Cert_File = /etc/ganesha/tls/tls_cert.pem;\n' + ' TLS_Key_File = /etc/ganesha/tls/tls_key.pem;\n' + ' TLS_CA_File = /etc/ganesha/tls/tls_ca_cert.pem;\n' + ' TLS_Ciphers = "ECDHE-ECDSA-AES256";\n' + ' TLS_Min_Version = "TLSv1.3";\n' + ' Enable_KTLS = True;\n' + ' Enable_debug = True;\n' + '}\n' + ) + assert expected_tls_block in ganesha_conf + class TestCephFsMirror: @patch("cephadm.serve.CephadmServe._run_cephadm") diff --git a/src/pybind/mgr/nfs/cluster.py b/src/pybind/mgr/nfs/cluster.py index 681adc65125..4f19d6d7f73 100644 --- a/src/pybind/mgr/nfs/cluster.py +++ b/src/pybind/mgr/nfs/cluster.py @@ -161,6 +161,14 @@ class NFSCluster: kmip_ca_cert: Optional[str] = None, kmip_host_list: Optional[List[Union[str, Dict[str, Union[str, int]]]]] = None, cluster_qos_config: Optional[Dict[str, Union[str, bool, int]]] = None, + ssl: bool = False, + ssl_cert: Optional[str] = None, + ssl_key: Optional[str] = None, + ssl_ca_cert: Optional[str] = None, + tls_ktls: bool = False, + tls_debug: bool = False, + tls_min_version: Optional[str] = None, + tls_ciphers: Optional[str] = None, ) -> None: if not port: port = 2049 # default nfs port @@ -199,7 +207,15 @@ class NFSCluster: kmip_key=kmip_key, kmip_ca_cert=kmip_ca_cert, kmip_host_list=kmip_host_list, - cluster_qos_config=cluster_qos_config) + cluster_qos_config=cluster_qos_config, + ssl=ssl, + ssl_cert=ssl_cert, + ssl_key=ssl_key, + ssl_ca_cert=ssl_ca_cert, + tls_ktls=tls_ktls, + tls_debug=tls_debug, + tls_min_version=tls_min_version, + tls_ciphers=tls_ciphers) completion = self.mgr.apply_nfs(spec) orchestrator.raise_if_exception(completion) ispec = IngressSpec(service_type='ingress', @@ -223,7 +239,15 @@ class NFSCluster: kmip_key=kmip_key, kmip_ca_cert=kmip_ca_cert, kmip_host_list=kmip_host_list, - cluster_qos_config=cluster_qos_config) + cluster_qos_config=cluster_qos_config, + ssl=ssl, + ssl_cert=ssl_cert, + ssl_key=ssl_key, + ssl_ca_cert=ssl_ca_cert, + tls_ktls=tls_ktls, + tls_debug=tls_debug, + tls_min_version=tls_min_version, + tls_ciphers=tls_ciphers) completion = self.mgr.apply_nfs(spec) orchestrator.raise_if_exception(completion) log.debug("Successfully deployed nfs daemons with cluster id %s and placement %s", @@ -253,6 +277,14 @@ class NFSCluster: kmip_ca_cert: Optional[str] = None, kmip_host_list: Optional[List[Union[str, Dict[str, Union[str, int]]]]] = None, cluster_qos_config: Optional[Dict[str, Union[str, bool, int]]] = None, + ssl: bool = False, + ssl_cert: Optional[str] = None, + ssl_key: Optional[str] = None, + ssl_ca_cert: Optional[str] = None, + tls_ktls: bool = False, + tls_debug: bool = False, + tls_min_version: Optional[str] = None, + tls_ciphers: Optional[str] = None, ) -> None: try: if virtual_ip: @@ -287,7 +319,15 @@ class NFSCluster: kmip_key, kmip_ca_cert, kmip_host_list, - cluster_qos_config=cluster_qos_config + cluster_qos_config=cluster_qos_config, + ssl=ssl, + ssl_cert=ssl_cert, + ssl_key=ssl_key, + ssl_ca_cert=ssl_ca_cert, + tls_ktls=tls_ktls, + tls_debug=tls_debug, + tls_min_version=tls_min_version, + tls_ciphers=tls_ciphers ) return raise NonFatalError(f"{cluster_id} cluster already exists") diff --git a/src/pybind/mgr/nfs/ganesha_conf.py b/src/pybind/mgr/nfs/ganesha_conf.py index 13365d8f788..bd59d8a0b65 100644 --- a/src/pybind/mgr/nfs/ganesha_conf.py +++ b/src/pybind/mgr/nfs/ganesha_conf.py @@ -48,7 +48,7 @@ def _validate_access_type(access_type: str) -> None: def _validate_sec_type(sec_type: str) -> None: - valid_sec_types = ["none", "sys", "krb5", "krb5i", "krb5p"] + valid_sec_types = ["none", "sys", "krb5", "krb5i", "krb5p", "tls", "mtls"] if not isinstance(sec_type, str) or sec_type not in valid_sec_types: raise NFSInvalidOperation( f"SecType {sec_type} invalid, valid types are {valid_sec_types}") diff --git a/src/pybind/mgr/nfs/module.py b/src/pybind/mgr/nfs/module.py index 9e232b80510..3733543226d 100644 --- a/src/pybind/mgr/nfs/module.py +++ b/src/pybind/mgr/nfs/module.py @@ -159,6 +159,9 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): """Create an NFS Cluster""" kmip_cert = kmip_key = kmip_ca_cert = kmip_host_list = None cluster_qos_config = None + ssl_cert = ssl_key = ssl_ca_cert = tls_min_version = tls_ciphers = None + ssl = tls_ktls = tls_debug = False + if inbuf: config = yaml.safe_load(inbuf) kmip_cert = config.get('kmip_cert') @@ -166,6 +169,14 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): kmip_ca_cert = config.get('kmip_ca_cert') kmip_host_list = config.get('kmip_host_list') cluster_qos_config = config.get('cluster_qos_config') + ssl = config.get('ssl') + ssl_cert = config.get('ssl_cert') + ssl_key = config.get('ssl_key') + ssl_ca_cert = config.get('ssl_ca_cert') + tls_min_version = config.get('tls_min_version') + tls_ktls = config.get('tls_ktls') + tls_debug = config.get('tls_debug') + tls_ciphers = config.get('tls_ciphers') return self.nfs.create_nfs_cluster(cluster_id=cluster_id, placement=placement, virtual_ip=virtual_ip, ingress=ingress, @@ -173,7 +184,15 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): enable_virtual_server=enable_virtual_server, kmip_cert=kmip_cert, kmip_key=kmip_key, kmip_ca_cert=kmip_ca_cert, kmip_host_list=kmip_host_list, - cluster_qos_config=cluster_qos_config) + cluster_qos_config=cluster_qos_config, + ssl=ssl, + ssl_cert=ssl_cert, + ssl_key=ssl_key, + ssl_ca_cert=ssl_ca_cert, + tls_ktls=tls_ktls, + tls_debug=tls_debug, + tls_min_version=tls_min_version, + tls_ciphers=tls_ciphers) @CLICommand('nfs cluster rm', perm='rw') @object_format.EmptyResponder() diff --git a/src/pybind/mgr/nfs/tests/test_nfs.py b/src/pybind/mgr/nfs/tests/test_nfs.py index cb5e94210b3..d73d0f254b0 100644 --- a/src/pybind/mgr/nfs/tests/test_nfs.py +++ b/src/pybind/mgr/nfs/tests/test_nfs.py @@ -904,7 +904,7 @@ NFS_CORE_PARAM { 'access_type': None, 'squash': None }], - 'sectype': ["krb5p", "krb5i", "sys"], + 'sectype': ["krb5p", "krb5i", "sys", "mtls", "tls"], 'fsal': { 'name': 'RGW', 'user_id': 'nfs.foo.bucket', @@ -918,7 +918,7 @@ NFS_CORE_PARAM { info = conf._get_export_dict(self.cluster_id, "/rgw/bucket") assert info["export_id"] == 2 assert info["path"] == "bucket" - assert info["sectype"] == ["krb5p", "krb5i", "sys"] + assert info["sectype"] == ["krb5p", "krb5i", "sys", "mtls", "tls"] def test_update_export_with_ganesha_conf(self): self._do_mock_test(self._do_test_update_export_with_ganesha_conf) diff --git a/src/python-common/ceph/deployment/service_spec.py b/src/python-common/ceph/deployment/service_spec.py index 26ccc960ae4..dd67aee4ff0 100644 --- a/src/python-common/ceph/deployment/service_spec.py +++ b/src/python-common/ceph/deployment/service_spec.py @@ -1393,6 +1393,16 @@ class NFSServiceSpec(ServiceSpec): kmip_ca_cert: Optional[str] = None, kmip_host_list: Optional[List[Union[str, Dict[str, Union[str, int]]]]] = None, cluster_qos_config: Optional[Dict[str, Union[str, bool, int]]] = None, + ssl: bool = False, + ssl_cert: Optional[str] = None, + ssl_key: Optional[str] = None, + ssl_ca_cert: Optional[str] = None, + certificate_source: Optional[str] = None, + custom_sans: Optional[List[str]] = None, + tls_ktls: bool = False, + tls_debug: bool = False, + tls_min_version: Optional[str] = None, + tls_ciphers: Optional[str] = None, ): assert service_type == 'nfs' super(NFSServiceSpec, self).__init__( @@ -1400,7 +1410,8 @@ class NFSServiceSpec(ServiceSpec): placement=placement, unmanaged=unmanaged, preview_only=preview_only, config=config, networks=networks, extra_container_args=extra_container_args, extra_entrypoint_args=extra_entrypoint_args, custom_configs=custom_configs, - ip_addrs=ip_addrs) + ip_addrs=ip_addrs, ssl=ssl, ssl_cert=ssl_cert, ssl_key=ssl_key, ssl_ca_cert=ssl_ca_cert, + certificate_source=certificate_source, custom_sans=custom_sans) self.port = port @@ -1433,6 +1444,12 @@ class NFSServiceSpec(ServiceSpec): f'kmip_host_list contains an invalid element: {host_obj}.' ) + # TLS fields + self.tls_ciphers = tls_ciphers + self.tls_ktls = tls_ktls + self.tls_debug = tls_debug + self.tls_min_version = tls_min_version + def get_port_start(self) -> List[int]: if self.port: return [self.port] @@ -1509,6 +1526,21 @@ class NFSServiceSpec(ServiceSpec): f"Invalid NFS spec: IOPS '{key}' should be an integer" ) + # TLS certificate validation + if self.ssl and not self.certificate_source: + raise SpecValidationError('If SSL is enabled, a certificate source must be provided.') + if self.certificate_source == CertificateSource.INLINE.value: + tls_field_names = [ + 'ssl_cert', + 'ssl_key', + 'ssl_ca_cert', + ] + tls_fields = [getattr(self, tls_field) for tls_field in tls_field_names] + if any(tls_fields) and not all(tls_fields): + raise SpecValidationError( + f'Either none or all of {tls_field_names} attributes must be set' + ) + yaml.add_representer(NFSServiceSpec, ServiceSpec.yaml_representer)