]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
test/mon: test cap enforcement for monitor subscriptions
authorPatrick Donnelly <pdonnell@ibm.com>
Thu, 28 May 2026 12:48:20 +0000 (08:48 -0400)
committerPatrick Donnelly <pdonnell@ibm.com>
Fri, 29 May 2026 19:36:58 +0000 (15:36 -0400)
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 <pdonnell@ibm.com>
src/test/mon/test-mon-msg.cc

index 2f83bff2a628aaeddc31c61b9f487697ba2008a8..9a341b00644d59978b5c2b591f9a61eb1a2ac861 100644 (file)
 #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<int> 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<CephContext> cct2;
+  MonClientHelper* helper2 = nullptr;
+  std::string entity;
+
+  void setup_low_priv_client(const std::string& ent, const std::string& caps) {
+    entity = ent;
+    std::vector<std::string> 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<CephContext>{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<std::string>("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<std::string> 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<std::string> 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<MKVData*>(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);