]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mds: add root_squash mode in MDS auth caps
authorRamana Raja <rraja@redhat.com>
Sun, 2 Aug 2020 12:53:43 +0000 (18:23 +0530)
committerRamana Raja <rraja@redhat.com>
Fri, 25 Sep 2020 09:04:35 +0000 (14:34 +0530)
Implement a root_squash mode in MDS auth caps to deny operations for
clients with uid=0 or gid=0 that need write access. It's mainly to
prevent operations such as accidental `sudo rm -rf /path`.

The root squash mode can be enforced in one of the following ways in
the MDS caps,
  'allow rw root_squash'
  (across file systems)
          or
  'allow rw fsname=a root_squash'
  (on a file system)
          or
  'allow rw fsname=a path=/vol/group/subvol00 root_squash'
  (on a file system path)

Fixes: https://tracker.ceph.com/issues/42451
Signed-off-by: Ramana Raja <rraja@redhat.com>
PendingReleaseNotes
doc/cephfs/client-auth.rst
qa/tasks/cephfs/test_admin.py
src/mds/MDSAuthCaps.cc
src/mds/MDSAuthCaps.h
src/mon/AuthMonitor.cc
src/test/mds/TestMDSAuthCaps.cc

index a2d176c331972750f62cd257e30a7c7bdbf2c1c4..e25c8319fee152b2f596375867df7e79fcc44146 100644 (file)
   ``fsname`` in caps. This also affects subcommand ``fs authorize``, the caps
   produce by it will be specific to the FS name passed in its arguments.
 
+* fs: root_squash flag can be set in MDS caps. It disallows file system
+  operations that need write access for clients with uid=0 or gid=0. This
+  feature should prevent accidents such as an inadvertent `sudo rm -rf /<path>`.
+
 * fs: "fs authorize" now sets MON cap to "allow <perm> fsname=<fsname>"
       instead of setting it to "allow r" all the time.
index 2a4fc1cd7dc8cc15ee1a2a7ece0c4799c08e57cb..3e8a791d3f58c9d5adf507895c4613612ccb582e 100644 (file)
@@ -226,3 +226,24 @@ But mounting ``cephfs2`` does not::
     $ sudo ceph-fuse /mnt/cephfs2 -n client.someuser -k ceph.client.someuser.keyring --client-fs=cephfs2
     ceph-fuse[96599]: starting ceph client
     ceph-fuse[96599]: ceph mount failed with (1) Operation not permitted
+
+Root squash
+===========
+
+The ``root squash`` feature is implemented as a safety measure to prevent
+scenarios such as accidental ``sudo rm -rf /path``. You can enable
+``root_squash`` mode in MDS caps to disallow clients with uid=0 or gid=0 to
+perform write access operations -- e.g., rm, rmdir, rmsnap, mkdir, mksnap.
+However, the mode allows the read operations of a root client unlike in
+other file systems.
+
+Following is an example of enabling root_squash in a filesystem except within
+'/volumes' directory tree in the filesystem::
+
+    $ ceph fs authorize a client.test_a / rw root_squash /volumes rw
+    $ ceph auth get client.test_a
+    [client.test_a]
+       key = AQBZcDpfEbEUKxAADk14VflBXt71rL9D966mYA==
+       caps mds = "allow rw fsname=a root_squash, allow rw fsname=a path=/volumes"
+       caps mon = "allow r fsname=a"
+       caps osd = "allow rw tag cephfs data=a"
index fc46952a5569a1445e2435d0ebda72ad9a692aa4..88fc349fcd7807f63969469b4e01e87df41580a6 100644 (file)
@@ -2,7 +2,7 @@ import json
 from io import StringIO
 from os.path import join as os_path_join
 
-from teuthology.orchestra.run import CommandFailedError
+from teuthology.orchestra.run import CommandFailedError, Raw
 
 from tasks.cephfs.cephfs_test_case import CephFSTestCase
 from tasks.cephfs.filesystem import FileLayout
@@ -433,6 +433,25 @@ class TestSubCmdFsAuthorize(CapsHelper):
         self.run_mon_cap_tests(moncap, keyring)
         self.run_mds_cap_tests(filepaths, filedata, mounts, perm)
 
+    def test_single_path_rootsquash(self):
+        filedata, filename = 'some data on fs 1', 'file_on_fs1'
+        filepath = os_path_join(self.mount_a.hostfs_mntpt, filename)
+        self.mount_a.write_file(filepath, filedata)
+
+        keyring = self.fs.authorize(self.client_id, ('/', 'rw', 'root_squash'))
+        keyring_path = self.create_keyring_file(self.mount_a.client_remote,
+                                                keyring)
+        self.mount_a.remount(client_id=self.client_id,
+                             client_keyring_path=keyring_path,
+                             cephfs_mntpt='/')
+
+        if filepath.find(self.mount_a.hostfs_mntpt) != -1:
+            # can read, but not write as root
+            contents = self.mount_a.read_file(filepath)
+            self.assertEqual(filedata, contents)
+            cmdargs = ['echo', 'some random data', Raw('|'), 'sudo', 'tee', filepath]
+            self.mount_a.negtestcmd(args=cmdargs, retval=1, errmsg='permission denied')
+
     def test_multiple_path_r(self):
         perm, paths = 'r', ('/dir1', '/dir2/dir22')
         filepaths, filedata, mounts, keyring = self.setup_test_env(perm, paths)
index 19c4bab0a0db6e5689bfb62fa1f4f40f5fae6cdd..51a919d2dbc8f91b7007063b6e0437cd342d528b 100644 (file)
@@ -43,6 +43,8 @@ struct MDSCapParser : qi::grammar<Iterator, MDSAuthCaps()>
 {
   MDSCapParser() : MDSCapParser::base_type(mdscaps)
   {
+    using qi::attr;
+    using qi::bool_;
     using qi::char_;
     using qi::int_;
     using qi::uint_;
@@ -65,16 +67,22 @@ struct MDSCapParser : qi::grammar<Iterator, MDSAuthCaps()>
     fs_name_str %= +char_("a-zA-Z0-9-_.");
 
     // match := [path=<path>] [uid=<uid> [gids=<gid>[,<gid>...]]
+    // TODO: allow fsname, and root_squash to be specified with uid, and gidlist
     path %= (spaces >> lit("path") >> lit('=') >> (quoted_path | unquoted_path));
     uid %= (spaces >> lit("uid") >> lit('=') >> uint_);
     uintlist %= (uint_ % lit(','));
     gidlist %= -(spaces >> lit("gids") >> lit('=') >> uintlist);
     fs_name %= -(spaces >> lit("fsname") >> lit('=') >> fs_name_str);
+    root_squash %= (spaces >> lit("root_squash") >> attr(true));
     match = -(
+             (fs_name >> path >> root_squash)[_val = phoenix::construct<MDSCapMatch>(_2, _1, _3)] |
             (uid >> gidlist)[_val = phoenix::construct<MDSCapMatch>(_1, _2)] |
             (path >> uid >> gidlist)[_val = phoenix::construct<MDSCapMatch>(_1, _2, _3)] |
              (fs_name >> path)[_val = phoenix::construct<MDSCapMatch>(_2, _1)] |
+             (fs_name >> root_squash)[_val = phoenix::construct<MDSCapMatch>(std::string(), _1, _2)] |
+             (path >> root_squash)[_val = phoenix::construct<MDSCapMatch>(_1, std::string(), _2)] |
              (path)[_val = phoenix::construct<MDSCapMatch>(_1)] |
+             (root_squash)[_val = phoenix::construct<MDSCapMatch>(std::string(), std::string(), _1)] |
              (fs_name)[_val = phoenix::construct<MDSCapMatch>(std::string(),
                                                              _1)]);
 
@@ -104,6 +112,7 @@ struct MDSCapParser : qi::grammar<Iterator, MDSAuthCaps()>
   qi::rule<Iterator> spaces;
   qi::rule<Iterator, string()> quoted_path, unquoted_path, network_str;
   qi::rule<Iterator, string()> fs_name_str, fs_name, path;
+  qi::rule<Iterator, bool()> root_squash;
   qi::rule<Iterator, MDSCapSpec()> capspec;
   qi::rule<Iterator, uint32_t()> uid;
   qi::rule<Iterator, std::vector<uint32_t>() > uintlist;
@@ -232,6 +241,10 @@ bool MDSAuthCaps::is_capable(std::string_view inode_path,
 
     if (grant.match.match(inode_path, caller_uid, caller_gid, caller_gid_list) &&
        grant.spec.allows(mask & (MAY_READ|MAY_EXECUTE), mask & MAY_WRITE)) {
+      if (grant.match.root_squash && ((caller_uid == 0) || (caller_gid == 0)) &&
+          (mask & MAY_WRITE)) {
+           continue;
+      }
       // we have a match; narrow down GIDs to those specifically allowed here
       vector<uint64_t> gids;
       if (std::find(grant.match.gids.begin(), grant.match.gids.end(), caller_gid) !=
@@ -362,14 +375,17 @@ bool MDSAuthCaps::allow_all() const
 
 ostream &operator<<(ostream &out, const MDSCapMatch &match)
 {
+  if (!match.fs_name.empty()) {
+    out << " fsname=" << match.fs_name;
+  }
   if (match.path.length()) {
-    out << "path=\"/" << match.path << "\"";
-    if (match.uid != MDSCapMatch::MDS_AUTH_UID_ANY) {
-      out << " ";
-    }
+    out << " path=\"/" << match.path << "\"";
+  }
+  if (match.root_squash) {
+    out << " root_squash";
   }
   if (match.uid != MDSCapMatch::MDS_AUTH_UID_ANY) {
-    out << "uid=" << match.uid;
+    out << " uid=" << match.uid;
     if (!match.gids.empty()) {
       out << " gids=";
       bool first = true;
@@ -413,9 +429,7 @@ ostream &operator<<(ostream &out, const MDSCapGrant &grant)
 {
   out << "allow ";
   out << grant.spec;
-  if (!grant.match.is_match_all()) {
-    out << " " << grant.match;
-  }
+  out << grant.match;
   if (grant.network.size()) {
     out << " network " << grant.network;
   }
index 06d6c7190f4be8ecfdd95490c0f739f805a2fd2a..eaac0918f315651b806d011344160e7caca197d8 100644 (file)
@@ -107,6 +107,12 @@ struct MDSCapMatch {
     normalize_path();
   }
 
+  explicit MDSCapMatch(std::string path, std::string fs_name, bool root_squash_) :
+    uid(MDS_AUTH_UID_ANY), path(std::move(path)), fs_name(std::move(fs_name)), root_squash(root_squash_)
+  {
+    normalize_path();
+  }
+
   MDSCapMatch(const std::string& path_, int64_t uid_, std::vector<gid_t>& gids_)
     : uid(uid_), gids(gids_), path(path_), fs_name(std::string()) {
     normalize_path();
@@ -137,6 +143,7 @@ struct MDSCapMatch {
   std::vector<gid_t> gids;  // Use these GIDs
   std::string path;  // Require path to be child of this (may be "" or "/" for any)
   std::string fs_name;
+  bool root_squash=false;
 };
 
 struct MDSCapGrant {
index 550a4b74367a4ba46ca08a27fcba30f78891d8d5..bdc0c64738ac22e786d50bd0dbad5547546e8320 100644 (file)
@@ -1356,7 +1356,8 @@ bool AuthMonitor::prepare_command(MonOpRequestRef op)
   }
 
   cmd_getval(cmdmap, "caps", caps_vec);
-  if ((caps_vec.size() % 2) != 0) {
+  // fs authorize command's can have odd number of caps arguments
+  if ((prefix != "fs authorize") && (caps_vec.size() % 2) != 0) {
     ss << "bad capabilities request; odd number of arguments";
     err = -EINVAL;
     goto done;
@@ -1621,6 +1622,11 @@ bool AuthMonitor::prepare_command(MonOpRequestRef op)
         it += 2) {
       const string &path = *it;
       const string &cap = *(it+1);
+      bool root_squash = false;
+      if ((it + 2) != caps_vec.end() && *(it+2) == "root_squash") {
+       root_squash = true;
+       ++it;
+      }
 
       if (cap != "r" && cap.compare(0, 2, "rw")) {
        ss << "Permission flags must start with 'r' or 'rw'.";
@@ -1660,6 +1666,10 @@ bool AuthMonitor::prepare_command(MonOpRequestRef op)
       if (path != "/") {
        mds_cap_string += " path=" + path;
       }
+
+      if (root_squash) {
+       mds_cap_string += " root_squash";
+      }
     }
 
     osd_cap_string += osd_cap_string.empty() ? "" : ", ";
index deaddf51c0878ad4f72b25adc4fa62128bf7537d..3556a8591c1b80f84165a3fec5b0427cd30de810 100644 (file)
@@ -46,6 +46,11 @@ const char *parse_good[] = {
   "allow r uid=1 gids=1,2,3, allow * uid=2",
   "allow r network 1.2.3.4/8",
   "allow rw path=/foo uid=1 gids=1,2,3 network 2.3.4.5/16",
+  "allow r root_squash",
+  "allow rw path=/foo root_squash",
+  "allow rw fsname=a root_squash",
+  "allow rw fsname=a path=/foo root_squash",
+  "allow rw fsname=a root_squash, allow rwp fsname=a path=/volumes",
   0
 };
 
@@ -232,6 +237,17 @@ TEST(MDSAuthCaps, AllowPathCharsQuoted) {
   ASSERT_FALSE(quo_cap.is_capable("foo", 0, 0, 0777, 0, 0, NULL, MAY_READ | MAY_WRITE, 0, 0, addr));
 }
 
+TEST(MDSAuthCaps, RootSquash) {
+  MDSAuthCaps rs_cap;
+  ASSERT_TRUE(rs_cap.parse(g_ceph_context, "allow rw root_squash, allow rw path=/sandbox", NULL));
+  ASSERT_TRUE(rs_cap.is_capable("foo", 0, 0, 0777, 0, 0, NULL, MAY_READ, 0, 0, addr));
+  ASSERT_TRUE(rs_cap.is_capable("foo", 0, 0, 0777, 10, 10, NULL, MAY_READ | MAY_WRITE, 0, 0, addr));
+  ASSERT_FALSE(rs_cap.is_capable("foo", 0, 0, 0777, 0, 0, NULL, MAY_READ | MAY_WRITE, 0, 0, addr));
+  ASSERT_TRUE(rs_cap.is_capable("sandbox", 0, 0, 0777, 0, 0, NULL, MAY_READ | MAY_WRITE, 0, 0, addr));
+  ASSERT_TRUE(rs_cap.is_capable("sandbox/foo", 0, 0, 0777, 0, 0, NULL, MAY_READ | MAY_WRITE, 0, 0, addr));
+  ASSERT_TRUE(rs_cap.is_capable("sandbox/foo", 0, 0, 0777, 10, 10, NULL, MAY_READ | MAY_WRITE, 0, 0, addr));
+}
+
 TEST(MDSAuthCaps, OutputParsed) {
   struct CapsTest {
     const char *input;
@@ -256,6 +272,12 @@ TEST(MDSAuthCaps, OutputParsed) {
      "MDSAuthCaps[allow * path=\"/foo\"]"},
     {"allow * path=\"/foo\"",
      "MDSAuthCaps[allow * path=\"/foo\"]"},
+    {"allow rw root_squash",
+     "MDSAuthCaps[allow rw root_squash]"},
+    {"allow rw fsname=a root_squash",
+     "MDSAuthCaps[allow rw fsname=a root_squash]"},
+    {"allow * path=\"/foo\" root_squash",
+     "MDSAuthCaps[allow * path=\"/foo\" root_squash]"},
     {"allow * path=\"/foo\" uid=1",
      "MDSAuthCaps[allow * path=\"/foo\" uid=1]"},
     {"allow * path=\"/foo\" uid=1 gids=1,2,3",
@@ -264,6 +286,8 @@ TEST(MDSAuthCaps, OutputParsed) {
      "MDSAuthCaps[allow r uid=1 gids=1,2,3, allow * uid=2]"},
     {"allow r uid=1 gids=1,2,3, allow * uid=2 network 10.0.0.0/8",
      "MDSAuthCaps[allow r uid=1 gids=1,2,3, allow * uid=2 network 10.0.0.0/8]"},
+    {"allow rw fsname=b, allow rw fsname=a root_squash",
+     "MDSAuthCaps[allow rw fsname=b, allow rw fsname=a root_squash]"},
   };
   size_t num_tests = sizeof(test_values) / sizeof(*test_values);
   for (size_t i = 0; i < num_tests; ++i) {