From: Patrick Donnelly Date: Thu, 28 May 2026 12:48:20 +0000 (-0400) Subject: test/mon: test cap enforcement for monitor subscriptions X-Git-Tag: testing/wip-pdonnell-testing-20260529.193802~3 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=49d8fadaac1f173ec2989cf2e2380ac1fdfa7bc2;p=ceph-ci.git test/mon: test cap enforcement for monitor subscriptions Add regression tests targeting unauthorized MMonSubscribe attempts on sensitive monitor endpoints (kv, mdsmap, osdmap, pg creates). Moves helper mechanics to MonClientHelper to support lower-privilege secondary clients. Fixes: https://tracker.ceph.com/issues/76964 Signed-off-by: Patrick Donnelly --- diff --git a/src/test/mon/test-mon-msg.cc b/src/test/mon/test-mon-msg.cc index 2f83bff2a62..9a341b00644 100644 --- a/src/test/mon/test-mon-msg.cc +++ b/src/test/mon/test-mon-msg.cc @@ -43,6 +43,14 @@ #include "messages/MRoute.h" #include "messages/MGenericMessage.h" #include "messages/MMonJoin.h" +#include "messages/MMonSubscribe.h" +#include "messages/MKVData.h" +#include "messages/MMDSMap.h" +#include "messages/MOSDMap.h" +#include "messages/MFSMap.h" +#include "messages/MOSDPGCreate2.h" +#include "auth/KeyRing.h" +#include "common/Cond.h" #define dout_context g_ceph_context #define dout_subsys ceph_subsys_ @@ -60,8 +68,11 @@ protected: MonClient monc; ceph::mutex lock = ceph::make_mutex("mon-msg-test::lock"); + ceph::condition_variable cond; set wanted; + int reply_type = 0; + Message *reply_msg = nullptr; public: @@ -73,6 +84,12 @@ public: monc(cct_, poolctx) { } + ~MonClientHelper() override { + if (reply_msg) { + reply_msg->put(); + reply_msg = nullptr; + } + } int post_init() { dout(1) << __func__ << dendl; @@ -175,7 +192,52 @@ fail: return err; } - virtual void handle_wanted(Message *m) { } + virtual void handle_wanted(Message *m) { + std::lock_guard l{lock}; + // caller will put() after they call us, so hold on to a ref + m->get(); + if (reply_msg) { + reply_msg->put(); + } + reply_msg = m; + cond.notify_all(); + } + + Message *send_wait_reply(Message *m, int t, double timeout=30.0) { + std::unique_lock l{lock}; + reply_type = t; + if (reply_msg) { + reply_msg->put(); + reply_msg = nullptr; + } + add_wanted(t); + send_message(m); + + std::cv_status status = std::cv_status::no_timeout; + if (timeout > 0) { + utime_t s = ceph_clock_now(); + status = cond.wait_for(l, ceph::make_timespan(timeout)); + utime_t e = ceph_clock_now(); + dout(20) << __func__ << " took " << (e-s) << " seconds" << dendl; + } else { + cond.wait(l); + } + rm_wanted(t); + l.unlock(); + if (status == std::cv_status::timeout) { + dout(20) << __func__ << " error: " << cpp_strerror(ETIMEDOUT) << dendl; + return (Message*)((long)-ETIMEDOUT); + } + + if (!reply_msg) + dout(20) << __func__ << " reply_msg is nullptr" << dendl; + else + dout(20) << __func__ << " reply_msg " << *reply_msg << dendl; + + Message *ret = reply_msg; + reply_msg = nullptr; + return ret; + } bool handle_message(Message *m) { dout(1) << __func__ << " " << *m << dendl; @@ -224,11 +286,6 @@ class MonMsgTest : public MonClientHelper, public ::testing::Test { protected: - int reply_type = 0; - Message *reply_msg = nullptr; - ceph::mutex lock = ceph::make_mutex("lock"); - ceph::condition_variable cond; - MonMsgTest() : MonClientHelper(g_ceph_context) { } @@ -249,43 +306,6 @@ public: reply_msg = nullptr; } } - - void handle_wanted(Message *m) override { - std::lock_guard l{lock}; - // caller will put() after they call us, so hold on to a ref - m->get(); - reply_msg = m; - cond.notify_all(); - } - - Message *send_wait_reply(Message *m, int t, double timeout=30.0) { - std::unique_lock l{lock}; - reply_type = t; - add_wanted(t); - send_message(m); - - std::cv_status status = std::cv_status::no_timeout; - if (timeout > 0) { - utime_t s = ceph_clock_now(); - status = cond.wait_for(l, ceph::make_timespan(timeout)); - utime_t e = ceph_clock_now(); - dout(20) << __func__ << " took " << (e-s) << " seconds" << dendl; - } else { - cond.wait(l); - } - rm_wanted(t); - l.unlock(); - if (status == std::cv_status::timeout) { - dout(20) << __func__ << " error: " << cpp_strerror(ETIMEDOUT) << dendl; - return (Message*)((long)-ETIMEDOUT); - } - - if (!reply_msg) - dout(20) << __func__ << " reply_msg is nullptr" << dendl; - else - dout(20) << __func__ << " reply_msg " << *reply_msg << dendl; - return reply_msg; - } }; TEST_F(MonMsgTest, MMonProbeTest) @@ -324,6 +344,195 @@ TEST_F(MonMsgTest, MMonJoin) ASSERT_FALSE(monc.monmap.contains("client")); } +class MonAuthBypassTest : public MonMsgTest { +protected: + boost::intrusive_ptr cct2; + MonClientHelper* helper2 = nullptr; + std::string entity; + + void setup_low_priv_client(const std::string& ent, const std::string& caps) { + entity = ent; + std::vector cmd = { + "{\"prefix\": \"auth get-or-create-key\", \"entity\": \"client." + entity + "\", \"caps\": " + caps + "}" + }; + bufferlist inbl, outbl; + string outs; + C_SaferCond cond_cmd; + monc.start_mon_command(std::move(cmd), std::move(inbl), &outbl, &outs, &cond_cmd); + ASSERT_EQ(0, cond_cmd.wait()); + + string key_str = outbl.to_str(); + key_str.erase(key_str.find_last_not_of(" \n\r\t") + 1); + + CephInitParameters iparams(CEPH_ENTITY_TYPE_CLIENT); + iparams.name.set(CEPH_ENTITY_TYPE_CLIENT, entity.c_str()); + cct2 = boost::intrusive_ptr{common_preinit(iparams, CODE_ENVIRONMENT_LIBRARY, 0), false}; + cct2->_conf.set_val("key", key_str); + cct2->_conf.set_val("debug_ms", "1"); + cct2->_conf.set_val("debug_auth", "20"); + cct2->_conf.set_val("debug_monc", "20"); + cct2->_conf.set_val("log_to_file", "false"); + cct2->_conf.set_val("log_to_stderr", "true"); + cct2->_conf.set_val("err_to_stderr", "true"); + cct2->_conf.set_val("mon_host", g_ceph_context->_conf.get_val("mon_host")); + cct2->_conf.apply_changes(nullptr); + + cct2->_log->start(); + common_init_finish(cct2.get()); + + helper2 = new MonClientHelper(cct2.get()); + ASSERT_EQ(0, helper2->init()); + } + + void teardown_low_priv_client() { + if (helper2) { + helper2->shutdown(); + delete helper2; + helper2 = nullptr; + } + if (!entity.empty()) { + std::vector cmd = { + "{\"prefix\": \"auth rm\", \"entity\": \"client." + entity + "\"}" + }; + bufferlist inbl, outbl; + string outs; + C_SaferCond cond_cmd; + monc.start_mon_command(std::move(cmd), std::move(inbl), &outbl, &outs, &cond_cmd); + EXPECT_EQ(0, cond_cmd.wait()); + entity.clear(); + } + if (cct2) { + cct2.reset(); + } + } + + void TearDown() override { + teardown_low_priv_client(); + MonMsgTest::TearDown(); + } +}; + +TEST_F(MonAuthBypassTest, MMonSubscribeKV_AuthBypass) +{ + setup_low_priv_client("test_kv_sub", "[\"mon\", \"allow r, allow service osd r, allow service mds r\"]"); + + std::vector cmd = { + "{\"prefix\": \"config-key set\", \"key\": \"test_secret\", \"val\": \"super_secret_value\"}" + }; + bufferlist inbl, outbl; + string outs; + C_SaferCond cond_cmd2; + monc.start_mon_command(std::move(cmd), std::move(inbl), &outbl, &outs, &cond_cmd2); + ASSERT_EQ(0, cond_cmd2.wait()); + + auto m = new MMonSubscribe(); + m->what["kv:"] = ceph_mon_subscribe_item(); + m->what["kv:"].start = 0; + m->what["kv:"].flags = 0; + + auto dummy_msg = new MKVData(); + int msg_kv_data_type = dummy_msg->get_type(); + dummy_msg->put(); + + Message *reply = helper2->send_wait_reply(m, msg_kv_data_type, 5.0); + + if (!IS_ERR(reply)) { + auto kv_reply = static_cast(reply); + + bool found_key = false; + std::string found_val; + + for (const auto& pair : kv_reply->data) { + if (pair.first.find("test_secret") != std::string::npos) { + found_key = true; + found_val = pair.second->to_str(); + break; + } + } + + if (found_key && found_val == "super_secret_value") { + ADD_FAILURE() << "Vulnerability Explicitly Confirmed! Exfiltrated 'test_secret' with value: '" + << found_val << "' using only 'mon allow r' caps!"; + } else if (found_key) { + ADD_FAILURE() << "Vulnerability present: Dumped key 'test_secret', but value was '" + << found_val << "'"; + } else { + ADD_FAILURE() << "Vulnerability present (received MKVData payload), but 'test_secret' wasn't found in the " + << kv_reply->data.size() << " dumped keys."; + } + + reply->put(); + } else { + ASSERT_EQ(PTR_ERR(reply), -ETIMEDOUT); + } + + cmd = { + "{\"prefix\": \"config-key rm\", \"key\": \"test_secret\"}" + }; + C_SaferCond cond_cmd4; + outbl.clear(); + inbl.clear(); + monc.start_mon_command(std::move(cmd), std::move(inbl), &outbl, &outs, &cond_cmd4); + ASSERT_EQ(0, cond_cmd4.wait()); +} + +TEST_F(MonAuthBypassTest, MMonSubscribeMDS_AuthBypass) +{ + setup_low_priv_client("test_mds_sub", "[\"mon\", \"allow service osd r, allow service mgr r\"]"); + + auto m = new MMonSubscribe(); + m->what["mdsmap"] = ceph_mon_subscribe_item(); + m->what["mdsmap"].start = 0; + m->what["mdsmap"].flags = 0; + + Message *reply = helper2->send_wait_reply(m, CEPH_MSG_MDS_MAP, 5.0); + + if (!IS_ERR(reply)) { + reply->put(); + ADD_FAILURE() << "Vulnerability present: received MDSMap with insufficient caps!"; + } else { + ASSERT_EQ(PTR_ERR(reply), -ETIMEDOUT); + } +} + +TEST_F(MonAuthBypassTest, MMonSubscribeOSD_AuthBypass) +{ + setup_low_priv_client("test_osd_sub", "[\"mon\", \"allow service mds r, allow service mgr r\"]"); + + auto m = new MMonSubscribe(); + m->what["osdmap"] = ceph_mon_subscribe_item(); + m->what["osdmap"].start = 0; + m->what["osdmap"].flags = 0; + + Message *reply = helper2->send_wait_reply(m, CEPH_MSG_OSD_MAP, 5.0); + + if (!IS_ERR(reply)) { + reply->put(); + ADD_FAILURE() << "Vulnerability present: received OSDMap with only 'mon allow r' caps!"; + } else { + ASSERT_EQ(PTR_ERR(reply), -ETIMEDOUT); + } +} + +TEST_F(MonAuthBypassTest, MMonSubscribeOSDPGCreates_AuthBypass) +{ + setup_low_priv_client("test_pg_sub", "[\"mon\", \"allow r, allow service mgr r\", \"osd\", \"allow r\"]"); + + auto m = new MMonSubscribe(); + m->what["osd_pg_creates"] = ceph_mon_subscribe_item(); + m->what["osd_pg_creates"].start = 0; + m->what["osd_pg_creates"].flags = 0; + + Message *reply = helper2->send_wait_reply(m, MSG_OSD_PG_CREATE2, 5.0); + + if (!IS_ERR(reply)) { + reply->put(); + ADD_FAILURE() << "Vulnerability present: PG create sub bypassed cap checks!"; + } else { + ASSERT_EQ(PTR_ERR(reply), -ETIMEDOUT); + } +} + int main(int argc, char *argv[]) { auto args = argv_to_vec(argc, argv);