From e1c52207809cbd6408311b01460d4fcc2f987b4c Mon Sep 17 00:00:00 2001 From: Ramana Raja Date: Sun, 2 Aug 2020 18:23:43 +0530 Subject: [PATCH] mds: add root_squash mode in MDS auth caps 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 --- PendingReleaseNotes | 4 ++++ doc/cephfs/client-auth.rst | 21 +++++++++++++++++++++ qa/tasks/cephfs/test_admin.py | 21 ++++++++++++++++++++- src/mds/MDSAuthCaps.cc | 30 ++++++++++++++++++++++-------- src/mds/MDSAuthCaps.h | 7 +++++++ src/mon/AuthMonitor.cc | 12 +++++++++++- src/test/mds/TestMDSAuthCaps.cc | 24 ++++++++++++++++++++++++ 7 files changed, 109 insertions(+), 10 deletions(-) diff --git a/PendingReleaseNotes b/PendingReleaseNotes index a2d176c331972..e25c8319fee15 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -158,5 +158,9 @@ ``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 /`. + * fs: "fs authorize" now sets MON cap to "allow fsname=" instead of setting it to "allow r" all the time. diff --git a/doc/cephfs/client-auth.rst b/doc/cephfs/client-auth.rst index 2a4fc1cd7dc8c..3e8a791d3f58c 100644 --- a/doc/cephfs/client-auth.rst +++ b/doc/cephfs/client-auth.rst @@ -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" diff --git a/qa/tasks/cephfs/test_admin.py b/qa/tasks/cephfs/test_admin.py index fc46952a5569a..88fc349fcd780 100644 --- a/qa/tasks/cephfs/test_admin.py +++ b/qa/tasks/cephfs/test_admin.py @@ -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) diff --git a/src/mds/MDSAuthCaps.cc b/src/mds/MDSAuthCaps.cc index 19c4bab0a0db6..51a919d2dbc8f 100644 --- a/src/mds/MDSAuthCaps.cc +++ b/src/mds/MDSAuthCaps.cc @@ -43,6 +43,8 @@ struct MDSCapParser : qi::grammar { 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 fs_name_str %= +char_("a-zA-Z0-9-_."); // match := [path=] [uid= [gids=[,...]] + // 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(_2, _1, _3)] | (uid >> gidlist)[_val = phoenix::construct(_1, _2)] | (path >> uid >> gidlist)[_val = phoenix::construct(_1, _2, _3)] | (fs_name >> path)[_val = phoenix::construct(_2, _1)] | + (fs_name >> root_squash)[_val = phoenix::construct(std::string(), _1, _2)] | + (path >> root_squash)[_val = phoenix::construct(_1, std::string(), _2)] | (path)[_val = phoenix::construct(_1)] | + (root_squash)[_val = phoenix::construct(std::string(), std::string(), _1)] | (fs_name)[_val = phoenix::construct(std::string(), _1)]); @@ -104,6 +112,7 @@ struct MDSCapParser : qi::grammar qi::rule spaces; qi::rule quoted_path, unquoted_path, network_str; qi::rule fs_name_str, fs_name, path; + qi::rule root_squash; qi::rule capspec; qi::rule uid; qi::rule() > 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 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; } diff --git a/src/mds/MDSAuthCaps.h b/src/mds/MDSAuthCaps.h index 06d6c7190f4be..eaac0918f3156 100644 --- a/src/mds/MDSAuthCaps.h +++ b/src/mds/MDSAuthCaps.h @@ -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& gids_) : uid(uid_), gids(gids_), path(path_), fs_name(std::string()) { normalize_path(); @@ -137,6 +143,7 @@ struct MDSCapMatch { std::vector 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 { diff --git a/src/mon/AuthMonitor.cc b/src/mon/AuthMonitor.cc index 550a4b74367a4..bdc0c64738ac2 100644 --- a/src/mon/AuthMonitor.cc +++ b/src/mon/AuthMonitor.cc @@ -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() ? "" : ", "; diff --git a/src/test/mds/TestMDSAuthCaps.cc b/src/test/mds/TestMDSAuthCaps.cc index deaddf51c0878..3556a8591c1b8 100644 --- a/src/test/mds/TestMDSAuthCaps.cc +++ b/src/test/mds/TestMDSAuthCaps.cc @@ -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) { -- 2.39.5