]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/nfs: Cephadm support for NFS-Ganesha TLS configuration adding new option xprtsec
authorShweta Bhosale <Shweta.Bhosale1@ibm.com>
Tue, 11 Nov 2025 07:47:31 +0000 (13:17 +0530)
committerShweta Bhosale <Shweta.Bhosale1@ibm.com>
Thu, 13 Nov 2025 09:13:14 +0000 (14:43 +0530)
Fixes: https://tracker.ceph.com/issues/73774
Signed-off-by: Shweta Bhosale <Shweta.Bhosale1@ibm.com>
Resolves: rhbz#2413723

 Conflicts:
src/pybind/mgr/nfs/export.py
src/pybind/mgr/nfs/ganesha_conf.py
src/pybind/mgr/nfs/module.py

doc/mgr/nfs.rst
src/pybind/mgr/nfs/export.py
src/pybind/mgr/nfs/ganesha_conf.py
src/pybind/mgr/nfs/module.py
src/pybind/mgr/nfs/tests/test_nfs.py

index 485200e1f5cc4a22a1e3963739494faabc903b21..2fc65e967dd75193173d71390ad0aea9aba26e0c 100644 (file)
@@ -290,7 +290,7 @@ Create CephFS Export
 
 .. prompt:: bash #
 
-   ceph nfs export create cephfs --cluster-id <cluster_id> --pseudo-path <pseudo_path> --fsname <fsname> [--readonly] [--path=/path/in/cephfs] [--client_addr <value>...] [--squash <value>] [--sectype <value>...] [--cmount_path <value>]
+   ceph nfs export create cephfs --cluster-id <cluster_id> --pseudo-path <pseudo_path> --fsname <fsname> [--readonly] [--path=/path/in/cephfs] [--client_addr <value>...] [--squash <value>] [--sectype <value>...] [--cmount_path <value>] [--xprtsec <value>]
 
 This creates export RADOS objects containing the export block, where
 
@@ -318,13 +318,16 @@ 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", "tls", "mtls"
-and "none". More than one value can be supplied. The flag may be specified
+connecting to the export. Valid values include ``krb5p``, ``krb5i``, ``krb5``, ``sys``
+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
 server will negotatiate a supported security type with the client preferring
 the supplied methods left-to-right.
 
+``<xprtsec>`` defines how traffic is secured at the transport layer.
+Valid values are ``tls``, ``mtls`` and ``none``.
+
 ``<cmount_path>`` specifies the path within the CephFS to mount this export on. It is
 allowed to be any complete path hierarchy between ``/`` and the ``EXPORT {path}``. (i.e. if ``EXPORT { Path }`` parameter is ``/foo/bar`` then cmount_path could be ``/``, ``/foo`` or ``/foo/bar``).
 
@@ -355,7 +358,7 @@ To export a *bucket*:
 
 .. prompt:: bash #
 
-   ceph nfs export create rgw --cluster-id <cluster_id> --pseudo-path <pseudo_path> --bucket <bucket_name> [--user-id <user-id>] [--readonly] [--client_addr <value>...] [--squash <value>] [--sectype <value>...]
+   ceph nfs export create rgw --cluster-id <cluster_id> --pseudo-path <pseudo_path> --bucket <bucket_name> [--user-id <user-id>] [--readonly] [--client_addr <value>...] [--squash <value>] [--sectype <value>...] [--xprtsec <value>]
 
 For example, to export ``mybucket`` via NFS cluster ``mynfs`` at the
 pseudo-path ``/bucketdata`` to any host in the ``192.168.10.0/24`` network
@@ -392,8 +395,8 @@ default 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", and "none". More than one value can be supplied. The flag may be
+connecting to the export. Valid values include ``krb5p``, ``krb5i``, ``krb5``,
+``sys`` 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 server will negotatiate a supported security type with the
@@ -403,6 +406,10 @@ client preferring the supplied methods left-to-right.
    function on servers that are configured to support Kerberos. Setting up
    NFS-Ganesha to support Kerberos is outside the scope of this document.
 
+``<xprtsec>`` defines how traffic is secured at the transport layer.
+Valid values are ``tls``, ``mtls`` and ``none``.
+
+
 RGW user export
 ^^^^^^^^^^^^^^^
 
index 42d0a1e9cdfbb1ffaec920811f49dcdceebe2ca5..3525bd4639e69d2ee591af2416b2642fd6f97a02 100644 (file)
@@ -675,6 +675,7 @@ class ExportMgr:
                              access_type: str,
                              clients: list = [],
                              sectype: Optional[List[str]] = None,
+                             xprtsec: Optional[str] = None,
                              cmount_path: Optional[str] = "/",
                              earmark_resolver: Optional[CephFSEarmarkResolver] = None,
                              kmip_key_id: Optional[str] = None
@@ -702,7 +703,8 @@ class ExportMgr:
                     },
                     "clients": clients,
                     "sectype": sectype,
-                    "kmip_key_id": kmip_key_id
+                    "kmip_key_id": kmip_key_id,
+                    "XprtSec": xprtsec,
                 },
                 earmark_resolver
             )
@@ -729,7 +731,8 @@ class ExportMgr:
                           user_id: Optional[str] = None,
                           clients: list = [],
                           sectype: Optional[List[str]] = None,
-                          kmip_key_id: Optional[str] = None) -> Dict[str, Any]:
+                          kmip_key_id: Optional[str] = None,
+                          xprtsec: Optional[str] = None) -> Dict[str, Any]:
         pseudo_path = normalize_path(pseudo_path)
 
         if not bucket and not user_id:
@@ -750,7 +753,8 @@ class ExportMgr:
                     },
                     "clients": clients,
                     "sectype": sectype,
-                    "kmip_key_id": kmip_key_id
+                    "kmip_key_id": kmip_key_id,
+                    "XprtSec": xprtsec,
                 }
             )
             log.debug("creating rgw export %s", export)
index bd59d8a0b65255ab187576ba047280acec8b9ad1..20c590ce66a4367bbb6c1140fb21c2fbdecc736f 100644 (file)
@@ -48,12 +48,19 @@ def _validate_access_type(access_type: str) -> None:
 
 
 def _validate_sec_type(sec_type: str) -> None:
-    valid_sec_types = ["none", "sys", "krb5", "krb5i", "krb5p", "tls", "mtls"]
+    valid_sec_types = ["none", "sys", "krb5", "krb5i", "krb5p"]
     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}")
 
 
+def _validate_xprtsec_type(xprtsec: str) -> None:
+    valid_xprtsec_types = ['none', 'tls', 'mtls']
+    if not isinstance(xprtsec, str) or xprtsec not in valid_xprtsec_types:
+        raise NFSInvalidOperation(
+            f"XprtSec {xprtsec} invalid, valid types are {valid_xprtsec_types}")
+
+
 class GaneshaConfParser:
     def __init__(self, raw_config: str):
         self.pos = 0
@@ -357,7 +364,8 @@ class Export:
             clients: Optional[List[Client]] = None,
             sectype: Optional[List[str]] = None,
             qos_block: Optional[QOS] = None,
-            kmip_key_id: Optional[str] = None) -> None:
+            kmip_key_id: Optional[str] = None,
+            xprtsec: Optional[str] = None) -> None:
         self.export_id = export_id
         self.path = path
         self.fsal = fsal
@@ -373,6 +381,7 @@ class Export:
         self.sectype = sectype
         self.qos_block = qos_block
         self.kmip_key_id = kmip_key_id
+        self.xprtsec = xprtsec
 
     @classmethod
     def from_export_block(cls, export_block: RawBlock, cluster_id: str) -> 'Export':
@@ -406,6 +415,9 @@ class Export:
         # https://github.com/ceph/go-ceph/issues/1097
         if sectype is not None and not isinstance(sectype, list):
             sectype = [sectype]
+
+        xprtsec = export_block.values.get('XprtSec')
+
         return cls(export_block.values['export_id'],
                    export_block.values['path'],
                    cluster_id,
@@ -420,8 +432,8 @@ class Export:
                     for client in client_blocks],
                    sectype=sectype,
                    qos_block=qos_block,
-                   kmip_key_id=export_block.values.get('kmip_key_id')
-                   )
+                   kmip_key_id=export_block.values.get('kmip_key_id'),
+                   xprtsec=xprtsec)
 
     def to_export_block(self) -> RawBlock:
         # if kmip_key_id is present, it should be first line of export block
@@ -442,6 +454,8 @@ class Export:
         })
         if self.sectype:
             values['SecType'] = self.sectype
+        if self.xprtsec:
+            values['XprtSec'] = self.xprtsec
         result = RawBlock("EXPORT", values=values)
         result.blocks = [
             self.fsal.to_fsal_block()
@@ -472,8 +486,8 @@ class Export:
                    [Client.from_dict(client) for client in ex_dict.get('clients', [])],
                    sectype=ex_dict.get("sectype"),
                    qos_block=qos_block,
-                   kmip_key_id=ex_dict.get('kmip_key_id')
-                   )
+                   kmip_key_id=ex_dict.get('kmip_key_id'),
+                   xprtsec=ex_dict.get('XprtSec'))
 
     def to_dict(self) -> Dict[str, Any]:
         values = {
@@ -495,6 +509,8 @@ class Export:
             values['qos_block'] = self.qos_block.to_dict()
         if self.kmip_key_id:
             values['kmip_key_id'] = self.kmip_key_id
+        if self.xprtsec:
+            values['XprtSec'] = self.xprtsec
         return values
 
     def validate(self, mgr: 'Module') -> None:
@@ -537,6 +553,8 @@ class Export:
 
         for st in (self.sectype or []):
             _validate_sec_type(st)
+        if self.xprtsec:
+            _validate_xprtsec_type(self.xprtsec)
 
     def __eq__(self, other: Any) -> bool:
         if not isinstance(other, Export):
index 3733543226d533f5b6bea230c6a274c66741531c..b888b320be27eb27e01df4f0dbeb8d3a0310408e 100644 (file)
@@ -43,7 +43,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
             sectype: Optional[List[str]] = None,
             cmount_path: Optional[str] = "/",
             skip_notify_nfs_server: bool = False,
-            kmip_key_id: Optional[str] = None
+            kmip_key_id: Optional[str] = None,
+            xprtsec: Optional[str] = None
     ) -> Dict[str, Any]:
         """Create a CephFS export"""
         self.export_mgr.skip_notify_nfs_server = skip_notify_nfs_server
@@ -58,6 +59,7 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
             squash=squash,
             addr=client_addr,
             sectype=sectype,
+            xprtsec=xprtsec,
             cmount_path=cmount_path,
             earmark_resolver=earmark_resolver,
             kmip_key_id=kmip_key_id
@@ -76,7 +78,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
             squash: str = 'none',
             sectype: Optional[List[str]] = None,
             skip_notify_nfs_server: bool = False,
-            kmip_key_id: Optional[str] = None
+            kmip_key_id: Optional[str] = None,
+            xprtsec: Optional[str] = None,
     ) -> Dict[str, Any]:
         """Create an RGW export"""
         self.export_mgr.skip_notify_nfs_server = skip_notify_nfs_server
@@ -90,7 +93,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
             squash=squash,
             addr=client_addr,
             sectype=sectype,
-            kmip_key_id=kmip_key_id
+            kmip_key_id=kmip_key_id,
+            xprtsec=xprtsec
         )
 
     @CLICommand('nfs export rm', perm='rw')
index d73d0f254b0c7d71e677f9513f21e89371412d4f..b6479908194676cc362cc76aa08b582bc5740f55 100644 (file)
@@ -888,6 +888,7 @@ NFS_CORE_PARAM {
         assert info["export_id"] == 2
         assert info["path"] == "bucket"
         assert "sectype" not in info
+        assert 'XprtSec' not in info
 
         r = conf.apply_export(self.cluster_id, json.dumps({
             'export_id': 2,
@@ -904,7 +905,8 @@ NFS_CORE_PARAM {
                 'access_type': None,
                 'squash': None
             }],
-            'sectype': ["krb5p", "krb5i", "sys", "mtls", "tls"],
+            'sectype': ["krb5p", "krb5i", "sys"],
+            'XprtSec': 'tls',
             'fsal': {
                 'name': 'RGW',
                 'user_id': 'nfs.foo.bucket',
@@ -918,7 +920,8 @@ 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", "mtls", "tls"]
+        assert info["sectype"] == ["krb5p", "krb5i", "sys"]
+        assert info['XprtSec'] == 'tls'
 
     def test_update_export_with_ganesha_conf(self):
         self._do_mock_test(self._do_test_update_export_with_ganesha_conf)
@@ -1105,7 +1108,8 @@ NFS_CORE_PARAM {
             pseudo_path='/mybucket',
             read_only=False,
             squash='root',
-            addr=["192.168.0.0/16"]
+            addr=["192.168.0.0/16"],
+            xprtsec='tls'
         )
         assert r["bind"] == "/mybucket"
 
@@ -1129,6 +1133,7 @@ NFS_CORE_PARAM {
         assert export.clients[0].access_type == 'rw'
         assert export.clients[0].addresses == ["192.168.0.0/16"]
         assert export.cluster_id == self.cluster_id
+        assert export.xprtsec == 'tls'
 
     def test_create_export_rgw_bucket_user(self):
         self._do_mock_test(self._do_test_create_export_rgw_bucket_user)