``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.
$ 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"
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
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)
{
MDSCapParser() : MDSCapParser::base_type(mdscaps)
{
+ using qi::attr;
+ using qi::bool_;
using qi::char_;
using qi::int_;
using qi::uint_;
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)]);
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;
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) !=
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;
{
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;
}
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();
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 {
}
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;
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'.";
if (path != "/") {
mds_cap_string += " path=" + path;
}
+
+ if (root_squash) {
+ mds_cap_string += " root_squash";
+ }
}
osd_cap_string += osd_cap_string.empty() ? "" : ", ";
"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
};
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;
"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",
"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) {