]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/cephadm: Cephadm support for NFS-Ganesha TLS configuration
authorShweta Bhosale <Shweta.Bhosale1@ibm.com>
Tue, 23 Sep 2025 15:50:04 +0000 (21:20 +0530)
committerShweta Bhosale <shbhosal@redhat.com>
Wed, 8 Oct 2025 14:25:41 +0000 (14:25 +0000)
Fixes: https://tracker.ceph.com/issues/73035
Signed-off-by: Shweta Bhosale <Shweta.Bhosale1@ibm.com>
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

13 files changed:
doc/cephadm/services/nfs.rst
doc/mgr/nfs.rst
src/cephadm/cephadmlib/daemons/nfs.py
src/pybind/mgr/cephadm/services/ingress.py
src/pybind/mgr/cephadm/services/nfs.py
src/pybind/mgr/cephadm/templates/services/nfs/ganesha.conf.j2
src/pybind/mgr/cephadm/tests/test_certmgr.py
src/pybind/mgr/cephadm/tests/test_services.py
src/pybind/mgr/nfs/cluster.py
src/pybind/mgr/nfs/ganesha_conf.py
src/pybind/mgr/nfs/module.py
src/pybind/mgr/nfs/tests/test_nfs.py
src/python-common/ceph/deployment/service_spec.py

index 110c83693398da364a0ef8d21acbfa5ec9dbae12..f7b2caabc37e56534a1bcf358dda085f2c02c934 100644 (file)
@@ -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 #
index 3a130f4f0a9a400714f7d2609159defe11c5279e..485200e1f5cc4a22a1e3963739494faabc903b21 100644 (file)
@@ -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.
 
 ``<sectype>`` 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 #
index 19751d3df216de1e02b2d40b70083cf63f201241..9be805a9d163d4a6cf0cb8507e6c717eb4b56fdf 100644 (file)
@@ -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:
index 35829f1ad7b33d0aec3c75aa9f1d344fb7be6a98..ecfafd8a9392f209ff683a8c5fc89c174542505f 100644 (file)
@@ -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)
 
index 04aa4be73f39393163bda8d335658e3965505d61..4226109db29bf57ed7d4d6ff3cce33cf751aadc6 100644 (file)
@@ -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,
index bd535054ef8897478b60ba80d7a77ca4e30763e6..35b0f6a0e4575c3dd896be265d69b169fa19dfdd 100644 (file)
@@ -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 }}
index b6b550456a48eafb7b3ef9e6ecdd37d4f804e951..0aa39a04de51c4c9ddf4990b2b41890250fb9b4d 100644 (file)
@@ -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
index 03dd968009d916fb171f33c337a3aefb03936766..39829c1b1fccfa7468c09aca7e3b22b93ac4fe11 100644 (file)
@@ -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")
index 681adc65125d1fe74562c1acdf0883de3b4f9710..4f19d6d7f730d0899d7545eb6d6abda78f200008 100644 (file)
@@ -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")
index 13365d8f7880af7ee21ab1febea92f61c0457c39..bd59d8a0b65255ab187576ba047280acec8b9ad1 100644 (file)
@@ -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}")
index 9e232b805100db1e7a37622ea2d3d22ee4b6b1fc..3733543226d533f5b6bea230c6a274c66741531c 100644 (file)
@@ -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()
index cb5e94210b3390abda9a49135992d4bc1f186f03..d73d0f254b0c7d71e677f9513f21e89371412d4f 100644 (file)
@@ -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)
index 26ccc960ae48170257a97b92a31d184f37fa1f73..dd67aee4ff0a1bf87340baea2484275dd9f4407d 100644 (file)
@@ -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)