[pkg-nagios-changes] [Git][nagios-team/icinga2][master] 4 commits: New upstream version 2.16.5

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Tue Aug 18 14:18:04 BST 2026



Bas Couwenberg pushed to branch master at Debian Nagios Maintainer Group / icinga2


Commits:
8639d086 by Bas Couwenberg at 2026-08-18T14:47:32+02:00
New upstream version 2.16.5
- - - - -
0dddee92 by Bas Couwenberg at 2026-08-18T14:48:00+02:00
Update upstream source from tag 'upstream/2.16.5'

Update to upstream version '2.16.5'
with Debian dir b518a74232ebf1e2bbe004304f97f08f19650fcd
- - - - -
4072ab1c by Bas Couwenberg at 2026-08-18T14:48:09+02:00
New upstream release.

- - - - -
158318e2 by Bas Couwenberg at 2026-08-18T14:49:38+02:00
Set distribution to unstable.

- - - - -


22 changed files:

- .github/workflows/authors-file.yml
- .github/workflows/backbot.yml
- .github/workflows/linux.yml
- .github/workflows/windows.yml
- CHANGELOG.md
- ICINGA2_VERSION
- debian/changelog
- lib/icingadb/icingadb-objects.cpp
- lib/icingadb/icingadb.cpp
- lib/icingadb/icingadb.hpp
- lib/remote/apilistener-configsync.cpp
- lib/remote/endpoint.cpp
- lib/remote/endpoint.hpp
- lib/remote/eventqueue.cpp
- lib/remote/eventqueue.hpp
- lib/remote/eventshandler.cpp
- lib/remote/filterutility.cpp
- lib/remote/filterutility.hpp
- lib/remote/jsonrpc.hpp
- lib/remote/jsonrpcconnection-pki.cpp
- lib/remote/jsonrpcconnection.cpp
- lib/remote/pkiutility.cpp


Changes:

=====================================
.github/workflows/authors-file.yml
=====================================
@@ -10,7 +10,7 @@ jobs:
 
     steps:
       - name: Checkout HEAD
-        uses: actions/checkout at v6
+        uses: actions/checkout at v7
         with:
           fetch-depth: 0
 


=====================================
.github/workflows/backbot.yml
=====================================
@@ -38,7 +38,7 @@ jobs:
           permission-issues: write # Needed to add comments to the PRs created by Backbot and the original PR.
 
       - name: Checkout
-        uses: actions/checkout at v6
+        uses: actions/checkout at v7
         with:
           token: ${{ steps.backbot-token.outputs.token }} # To make authenticated git operations.
           sha: ${{ github.event.pull_request.head.sha }} # Checkout the latest commit of the merged PR.


=====================================
.github/workflows/linux.yml
=====================================
@@ -33,21 +33,18 @@ jobs:
           - debian:12
           - debian:13
 
-          - fedora:41
           - fedora:42
           - fedora:43
           - fedora:44
           - fedora:45
 
-          - opensuse/leap:15.6
-          - opensuse/leap:16.0
-
           # We don't actually support Rocky Linux as such!
           # We just use that RHEL clone to test the original.
           - rockylinux:8
           - rockylinux:9
           - rockylinux/rockylinux:10
 
+          # openSUSE Leap is close enough to SLES to test just one of them.
           - registry.suse.com/suse/sle15:15.6
           - registry.suse.com/suse/sle15:15.7
           - registry.suse.com/bci/bci-base:16.0
@@ -69,13 +66,13 @@ jobs:
 
     steps:
       - name: Checkout HEAD
-        uses: actions/checkout at v6
+        uses: actions/checkout at v7
 
       - name: Turn on Problem Matcher
         run: echo "::add-matcher::.github/problem-matchers/gcc.json"
 
       - name: Restore/backup ccache
-        uses: actions/cache at v5
+        uses: actions/cache at v6
         with:
           path: ccache
           key: ccache/${{ matrix.distro }}


=====================================
.github/workflows/windows.yml
=====================================
@@ -29,7 +29,7 @@ jobs:
 
     steps:
       - name: Checkout HEAD
-        uses: actions/checkout at v6
+        uses: actions/checkout at v7
         with:
           fetch-depth: 0
 


=====================================
CHANGELOG.md
=====================================
@@ -7,6 +7,33 @@ documentation before upgrading to a new release.
 
 Released closed milestones can be found on [GitHub](https://github.com/Icinga/icinga2/milestones?state=closed).
 
+## 2.16.5 (2026-08-18)
+
+This release contains a bugfix for a regression in the `IcingaDB` feature that was introduced in v2.16.0 and some
+security enhancements that fix a couple of minor vulnerabilities.
+
+One vulnerability allowed an authenticated `ApiUser` with the `events/*` permission to obtain information about
+objects through crafted filter expressions, without holding the relevant `objects/query/*` permission. The fix is to
+correctly apply permissions to filter expressions on `/v1/events`.
+
+The other vulnerability allowed an authenticated Icinga 2 node to use the ~1 GB message limit to possibly crash another
+node through memory exhaustion. Depending on available memory, multiple compromised nodes may be required for
+a successful attack since a node can only make one connection. The fix applies a 16 MiB limit to messages from nodes
+lower in the hierarchy.
+
+### Security Enhancements
+
+* Apply user permissions to filter expressions for `/v1/events` similarly to `/v1/objects`
+  ([GHSA-v265-w3gm-99vg](https://github.com/Icinga/icinga2/security/advisories/GHSA-v265-w3gm-99vg))
+* Introduce an additional 16 MiB message size limit on all child-zone connections
+  ([GHSA-wm63-p2jg-5665](https://github.com/Icinga/icinga2/security/advisories/GHSA-wm63-p2jg-5665))
+* Don't include sensitive certificate request tickets in log messages (#10960)
+* Don't log full object config containing potentially sensitive information (#10988)
+
+### Bugfixes
+
+* IcingaDB: Fix multiple potential race conditions during initial config dump (#10981)
+
 ## 2.16.4 (2026-07-16)
 
 This release contains a number of fixes for various smaller but annoying bugs, including one regression regarding API


=====================================
ICINGA2_VERSION
=====================================
@@ -1,2 +1,2 @@
-Version: 2.16.4
+Version: 2.16.5
 Revision: 1


=====================================
debian/changelog
=====================================
@@ -1,3 +1,9 @@
+icinga2 (2.16.5-1) unstable; urgency=medium
+
+  * New upstream release.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Tue, 18 Aug 2026 14:49:23 +0200
+
 icinga2 (2.16.4-1) unstable; urgency=medium
 
   * New upstream release.


=====================================
lib/icingadb/icingadb-objects.cpp
=====================================
@@ -210,6 +210,11 @@ void IcingaDB::ConfigStaticInitialize()
 
 void IcingaDB::UpdateAllConfigObjects()
 {
+	// Snapshot the current number of reconnections made by any of the RedisConnections.
+	// This is compared to the current value at the end of the function to verify that no
+	// reconnects occurred.
+	auto snapReconnectCount = m_ReconnectCount->load(std::memory_order_acquire);
+
 	// This function performs an initial dump of all configuration objects into Redis, thus there are no
 	// previously enqueued queries on m_RconWorker that we need to wait for. So, no Sync() call is necessary here.
 	m_RconWorker->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "6"}, {}, true);
@@ -513,13 +518,8 @@ void IcingaDB::UpdateAllConfigObjects()
 
 	if (upq.HasExceptions()) {
 		for (std::exception_ptr exc : upq.GetExceptions()) {
-			try {
-				if (exc) {
-					std::rethrow_exception(exc);
-			}
-			} catch(const std::exception& e) {
-				Log(LogCritical, "IcingaDB")
-						<< "Exception during ConfigDump: " << e.what();
+			if (exc) {
+				std::rethrow_exception(exc);
 			}
 		}
 	}
@@ -530,10 +530,21 @@ void IcingaDB::UpdateAllConfigObjects()
 
 	m_RconWorker->FireAndForgetQuery({"XADD", "icinga:dump", "*", "key", "*", "state", "done"});
 
-	// enqueue a callback that will notify us once all previous queries were executed and wait for this event
-	std::promise<void> p;
-	m_RconWorker->EnqueueCallback([&p](boost::asio::yield_context&) { p.set_value(); });
-	p.get_future().wait();
+	/* The other connections already get a final sync in their workqueue above, so we sync just
+	 * the `m_RconWorker` here, to ensure that all requests have made it through the queue.
+	 * This also puts a final check to the connection, since it would throw if the connection
+	 * went down in the meantime.
+	 */
+	m_RconWorker->Sync();
+
+	/* Since now all connections were synced after all transactions have completed, we can now compare
+	 * the current reconnect count to the snapshot we made at the start of this function. If the count
+	 * matches we know for sure that no reconnects on any of the connections lead to an inconsistent
+	 * sync. If reconnects did occur, this throws and the update is repeated again.
+	 */
+	if (m_ReconnectCount->load(std::memory_order_acquire) != snapReconnectCount) {
+		BOOST_THROW_EXCEPTION(std::runtime_error{"Lost connection to Redis during ConfigDump"});
+	}
 
 	auto endTime (Utility::GetTime());
 	auto took (endTime - startTime);


=====================================
lib/icingadb/icingadb.cpp
=====================================
@@ -84,6 +84,8 @@ void IcingaDB::Start(bool runtimeCreated)
 	m_RconLocked.store(m_Rcon);
 
 	m_RconWorker = new RedisConnection(connInfo, m_Rcon);
+	std::vector<RedisConnection::Ptr> childConns;
+	childConns.push_back(m_RconWorker);
 
 	for (const auto& [type, _] : GetSyncableTypes()) {
 		auto ctype (dynamic_cast<ConfigType*>(type.get()));
@@ -91,28 +93,32 @@ void IcingaDB::Start(bool runtimeCreated)
 			continue;
 
 		RedisConnection::Ptr con = new RedisConnection(connInfo, m_Rcon);
+		childConns.push_back(con);
 
-		con->SetConnectedCallback([this, con](boost::asio::yield_context&) {
-			con->SetConnectedCallback(nullptr);
+		m_Rcons[ctype] = std::move(con);
+	}
+
+	auto pendingConns = std::make_shared<std::atomic_size_t>(childConns.size());
 
-			size_t pending = --m_PendingRcons;
+	for (auto & conn : childConns) {
+		conn->SetConnectedCallback([this, pendingConns, conn](boost::asio::yield_context&) {
+			conn->SetConnectedCallback([reconnectCount = m_ReconnectCount](boost::asio::yield_context&) {
+				reconnectCount->fetch_add(1, std::memory_order_release);
+			});
+
+			auto pending = --*pendingConns;
 			Log(LogDebug, "IcingaDB") << pending << " pending child connections remaining";
 			if (pending == 0) {
 				m_WorkQueue.Enqueue([this]() { OnConnectedHandler(); });
 			}
 		});
-
-		m_Rcons[ctype] = std::move(con);
 	}
 
-	m_PendingRcons = m_Rcons.size();
-
-	m_Rcon->SetConnectedCallback([this](boost::asio::yield_context&) {
+	m_Rcon->SetConnectedCallback([this, childConns = std::move(childConns)](boost::asio::yield_context&) {
 		m_Rcon->SetConnectedCallback(nullptr);
 
-		m_RconWorker->Start();
-		for (auto& kv : m_Rcons) {
-			kv.second->Start();
+		for (const auto& conn : childConns) {
+			conn->Start();
 		}
 	});
 	m_Rcon->Start();
@@ -149,7 +155,15 @@ void IcingaDB::OnConnectedHandler()
 	m_ConfigDumpInProgress = true;
 	PublishStats();
 
-	UpdateAllConfigObjects();
+	while (true) {
+		try {
+			UpdateAllConfigObjects();
+			break;
+		} catch (const std::exception& ex) {
+			Log(LogCritical, "IcingaDB") << "Exception during ConfigDump: " << ex.what();
+		}
+		Utility::Sleep(10);
+	}
 
 	m_ConfigDumpDone.store(true);
 	m_ConfigDumpInProgress = false;


=====================================
lib/icingadb/icingadb.hpp
=====================================
@@ -456,7 +456,7 @@ private:
 	 */
 	RedisConnection::Ptr m_RconWorker;
 	std::unordered_map<ConfigType*, RedisConnection::Ptr> m_Rcons;
-	std::atomic_size_t m_PendingRcons;
+	std::shared_ptr<std::atomic_uint> m_ReconnectCount = std::make_shared<std::atomic_uint>(0);
 
 	struct {
 		DumpedGlobals CustomVar, ActionUrl, NotesUrl, IconImage, DependencyGroup;


=====================================
lib/remote/apilistener-configsync.cpp
=====================================
@@ -42,9 +42,6 @@ void ApiListener::ConfigUpdateObjectHandler(const ConfigObject::Ptr& object, con
 
 Value ApiListener::ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
 {
-	Log(LogNotice, "ApiListener")
-		<< "Received config update for object: " << JsonEncode(params);
-
 	/* check permissions */
 	ApiListener::Ptr listener = ApiListener::GetInstance();
 
@@ -108,6 +105,10 @@ Value ApiListener::ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin
 		return Empty;
 	}
 
+	Log(LogNotice, "ApiListener")
+		<< "Received config update for object '" << objName << "' of type " << objType
+		<< ", version: " << std::fixed << objVersion;
+
 	Type::Ptr ptype = Type::GetByName(objType);
 	auto *ctype = dynamic_cast<ConfigType *>(ptype.get());
 


=====================================
lib/remote/endpoint.cpp
=====================================
@@ -182,3 +182,14 @@ double Endpoint::GetSecondsProcessingMessages() const
 {
 	return m_InputProcessingTime;
 }
+
+ssize_t Endpoint::GetMessageReceiveSizeLimit() const
+{
+	// Parent and sibling nodes are trusted
+	// to send messages of unbounded size, e.g. config syncs
+	if (Zone::GetLocalZone()->IsChildOf(m_Zone)) {
+		return -1;
+	}
+
+	return 16L * 1024 * 1024;
+}


=====================================
lib/remote/endpoint.hpp
=====================================
@@ -62,6 +62,8 @@ public:
 
 	double GetSecondsProcessingMessages() const override;
 
+	ssize_t GetMessageReceiveSizeLimit() const;
+
 protected:
 	void OnAllConfigLoaded() override;
 


=====================================
lib/remote/eventqueue.cpp
=====================================
@@ -130,8 +130,8 @@ std::map<String, EventsInbox::Filter> EventsInbox::m_Filters ({{"", EventsInbox:
 
 EventsRouter EventsRouter::m_Instance;
 
-EventsInbox::EventsInbox(String filter, const String& filterSource)
-	: m_Timer(IoEngine::Get().GetIoContext())
+EventsInbox::EventsInbox(String filter, const String& filterSource, ApiUser::Ptr user)
+	: m_User(std::move(user)), m_Timer(IoEngine::Get().GetIoContext())
 {
 	std::unique_lock<std::mutex> lock (m_FiltersMutex);
 	m_Filter = m_Filters.find(filter);
@@ -169,6 +169,11 @@ const Expression::Ptr& EventsInbox::GetFilter()
 	return m_Filter->second.Expr;
 }
 
+const ApiUser::Ptr& EventsInbox::GetUser() const noexcept
+{
+	return m_User;
+}
+
 void EventsInbox::Push(Dictionary::Ptr event)
 {
 	std::unique_lock<std::mutex> lock (m_Mutex);
@@ -214,8 +219,8 @@ Dictionary::Ptr EventsInbox::Shift(boost::asio::yield_context yc, double timeout
 	return event;
 }
 
-EventsSubscriber::EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource)
-	: m_Types(std::move(types)), m_Inbox(new EventsInbox(std::move(filter), filterSource))
+EventsSubscriber::EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource, ApiUser::Ptr user)
+	: m_Types(std::move(types)), m_Inbox(new EventsInbox(std::move(filter), filterSource, std::move(user)))
 {
 	EventsRouter::GetInstance().Subscribe(m_Types, m_Inbox);
 }
@@ -244,22 +249,32 @@ void EventsFilter::Push(Dictionary::Ptr event)
 {
 	for (auto& perFilter : m_Inboxes) {
 		if (perFilter.first) {
-			ScriptFrame frame(true, new Namespace());
-			frame.Sandboxed = true;
-
-			try {
-				if (!FilterUtility::EvaluateFilter(frame, perFilter.first.get(), event, "event")) {
-					continue;
+			/* Each subscriber may hold different permissions, so the filter has to be evaluated
+			 * separately per inbox using a checker bound to that inbox's own user, even though
+			 * several inboxes here share the same compiled filter expression. A fresh checker is
+			 * created per evaluation since inboxes are shared across concurrently dispatching
+			 * threads and the checker's internal permission cache is not thread-safe.
+			 */
+			for (auto& inbox : perFilter.second) {
+				ScriptFrame frame(true, new Namespace());
+
+				frame.Sandboxed = true;
+				frame.PermChecker = new FilterExprPermissionChecker(inbox->GetUser());
+
+				try {
+					if (FilterUtility::EvaluateFilter(frame, perFilter.first.get(), event, "event")) {
+						inbox->Push(event);
+					}
+				} catch (const std::exception& ex) {
+					Log(LogWarning, "EventQueue")
+						<< "Error occurred while evaluating event filter for queue of user '"
+						<< inbox->GetUser()->GetName() << "': " << DiagnosticInformation(ex);
 				}
-			} catch (const std::exception& ex) {
-				Log(LogWarning, "EventQueue")
-					<< "Error occurred while evaluating event filter for queue: " << DiagnosticInformation(ex);
-				continue;
 			}
-		}
-
-		for (auto& inbox : perFilter.second) {
-			inbox->Push(event);
+		} else {
+			for (auto& inbox : perFilter.second) {
+				inbox->Push(event);
+			}
 		}
 	}
 }


=====================================
lib/remote/eventqueue.hpp
=====================================
@@ -86,7 +86,7 @@ class EventsInbox : public Object
 public:
 	DECLARE_PTR_TYPEDEFS(EventsInbox);
 
-	EventsInbox(String filter, const String& filterSource);
+	EventsInbox(String filter, const String& filterSource, ApiUser::Ptr user);
 	EventsInbox(const EventsInbox&) = delete;
 	EventsInbox(EventsInbox&&) = delete;
 	EventsInbox& operator=(const EventsInbox&) = delete;
@@ -94,6 +94,7 @@ public:
 	~EventsInbox();
 
 	const Expression::Ptr& GetFilter();
+	const ApiUser::Ptr& GetUser() const noexcept;
 
 	void Push(Dictionary::Ptr event);
 	Dictionary::Ptr Shift(boost::asio::yield_context yc, double timeout = 5);
@@ -110,6 +111,7 @@ private:
 
 	std::mutex m_Mutex;
 	decltype(m_Filters.begin()) m_Filter;
+	ApiUser::Ptr m_User;
 	std::queue<Dictionary::Ptr> m_Queue;
 	boost::asio::deadline_timer m_Timer;
 };
@@ -117,7 +119,7 @@ private:
 class EventsSubscriber
 {
 public:
-	EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource);
+	EventsSubscriber(std::set<EventType> types, String filter, const String& filterSource, ApiUser::Ptr user);
 	EventsSubscriber(const EventsSubscriber&) = delete;
 	EventsSubscriber(EventsSubscriber&&) = delete;
 	EventsSubscriber& operator=(const EventsSubscriber&) = delete;


=====================================
lib/remote/eventshandler.cpp
=====================================
@@ -103,7 +103,7 @@ bool EventsHandler::HandleRequest(
 		filter = HttpUtility::GetLastParameter(params, "filter");
 	}
 
-	EventsSubscriber subscriber (std::move(eventTypes), std::move(filter), l_ApiQuery);
+	EventsSubscriber subscriber (std::move(eventTypes), std::move(filter), l_ApiQuery, user);
 
 	response.result(http::status::ok);
 	response.set(http::field::content_type, "application/json");


=====================================
lib/remote/filterutility.cpp
=====================================
@@ -26,110 +26,95 @@ Dictionary::Ptr FilterUtility::GetTargetForVar(const String& name, const Value&
 	});
 }
 
+FilterExprPermissionChecker::FilterExprPermissionChecker(ApiUser::Ptr user) : m_User(std::move(user))
+{
+}
+
 /**
- * Controls access to an object or variable based on an ApiUser's permissions.
+ * Check if the user has the given permission and cache the result if they do.
+ *
+ * This is a wrapper around FilterUtility::CheckPermission() that caches the generated
+ * filter expression for later use when checking permissions inside sandboxed ScriptFrames.
+ *
+ * Like FilterUtility::CheckPermission() an exception is thrown if the user does not have
+ * the requested permission.
+ *
+ * If the user has permission and there is a filter for the given permission, the filter
+ * expression  is generated, cached and then a pointer to it is returned, otherwise a
+ * nullptr will be returned.
+ *
+ * Since the optionally returned pointer is a raw-pointer and this class retains ownership
+ * over the expression it is only valid for the lifetime of the @c FilterExprPermissionChecker
+ * object that returned it.
+ *
+ * @param permissionString The permission string to check against the ApiUser member of this class.
  *
- * This is accomplished by caching the generated filter expressions so they don't have to be
- * regenerated again and again when access is repeatedly checked in script functions and when
- * evaluating expressions.
+ * @return a pointer to the generated permission expression if the permission has a filter, or nullptr if not.
  */
-class FilterExprPermissionChecker : public ScriptPermissionChecker
+Expression* FilterExprPermissionChecker::CheckPermission(const String& permissionString)
 {
-public:
-	DECLARE_PTR_TYPEDEFS(FilterExprPermissionChecker);
-
-	explicit FilterExprPermissionChecker(ApiUser::Ptr user) : m_User(std::move(user)) {}
-
-	/**
-	 * Check if the user has the given permission and cache the result if they do.
-	 *
-	 * This is a wrapper around FilterUtility::CheckPermission() that caches the generated
-	 * filter expression for later use when checking permissions inside sandboxed ScriptFrames.
-	 *
-	 * Like FilterUtility::CheckPermission() an exception is thrown if the user does not have
-	 * the requested permission.
-	 *
-	 * If the user has permission and there is a filter for the given permission, the filter
-	 * expression  is generated, cached and then a pointer to it is returned, otherwise a
-	 * nullptr will be returned.
-	 *
-	 * Since the optionally returned pointer is a raw-pointer and this class retains ownership
-	 * over the expression it is only valid for the lifetime of the @c FilterExprPermissionChecker
-	 * object that returned it.
-	 *
-	 * @param permissionString The permission string to check against the ApiUser member of this class.
-	 *
-	 * @return a pointer to the generated permission expression if the permission has a filter, or nullptr if not.
-	 */
-	Expression* CheckPermission(const String& permissionString)
-	{
-		auto [it, inserted] = m_PermCache.try_emplace(permissionString);
-		auto& [hasPermission, permissionExpr] = it->second;
-
-		if (inserted) {
-			FilterUtility::CheckPermission(m_User, permissionString, &permissionExpr);
-		} else if (!hasPermission) {
-			BOOST_THROW_EXCEPTION(ScriptError("Missing permission: " + permissionString.ToLower()));
-		}
+	auto [it, inserted] = m_PermCache.try_emplace(permissionString);
+	auto& [hasPermission, permissionExpr] = it->second;
 
-		hasPermission = true;
-		return permissionExpr.get();
+	if (inserted) {
+		FilterUtility::CheckPermission(m_User, permissionString, &permissionExpr);
+	} else if (!hasPermission) {
+		BOOST_THROW_EXCEPTION(ScriptError("Missing permission: " + permissionString.ToLower()));
 	}
 
-	/**
-	 * Checks if this object's ApiUser has permissions to access variable `varName`.
-	 *
-	 * @param varName The name of the variable to check for access
-	 *
-	 * @return 'true' if the variable can be accessed, 'false' if it can't.
-	 */
-	bool CanAccessGlobalVariable(const String& varName) override
-	{
-		auto obj = FilterUtility::GetTargetForVar(varName, ScriptGlobal::Get(varName));
-		return CheckPermissionAndEvalFilter("variables", obj, "variable");
-	}
+	hasPermission = true;
+	return permissionExpr.get();
+}
 
-	/**
-	 * Checks if this object's ApiUser has permissions to access ConfigObject `obj`.
-	 *
-	 * @param obj A pointer to the ConfigObject to check for access
-	 *
-	 * @return 'true' if the object can be accessed, 'false' if it can't.
-	 */
-	bool CanAccessConfigObject(const ConfigObject::Ptr& obj) override
-	{
-		ASSERT(obj);
-
-		String perm = "objects/query/" + obj->GetReflectionType()->GetName();
-		String varName = obj->GetReflectionType()->GetName().ToLower();
-
-		return CheckPermissionAndEvalFilter(perm, obj, varName);
-	}
+/**
+ * Checks if this object's ApiUser has permissions to access variable `varName`.
+ *
+ * @param varName The name of the variable to check for access
+ *
+ * @return 'true' if the variable can be accessed, 'false' if it can't.
+ */
+bool FilterExprPermissionChecker::CanAccessGlobalVariable(const String& varName)
+{
+	auto obj = FilterUtility::GetTargetForVar(varName, ScriptGlobal::Get(varName));
+	return CheckPermissionAndEvalFilter("variables", obj, "variable");
+}
 
-private:
-	bool CheckPermissionAndEvalFilter(const String& permissionString, const Object::Ptr& obj, const String& varName)
-	{
-		auto [it, inserted] = m_PermCache.try_emplace(permissionString);
-		auto& [hasPermission, permissionExpr] = it->second;
+/**
+ * Checks if this object's ApiUser has permissions to access ConfigObject `obj`.
+ *
+ * @param obj A pointer to the ConfigObject to check for access
+ *
+ * @return 'true' if the object can be accessed, 'false' if it can't.
+ */
+bool FilterExprPermissionChecker::CanAccessConfigObject(const ConfigObject::Ptr& obj)
+{
+	ASSERT(obj);
 
-		if (inserted) {
-			hasPermission = FilterUtility::HasPermission(m_User, permissionString, &permissionExpr);
-		}
+	String perm = "objects/query/" + obj->GetReflectionType()->GetName();
+	String varName = obj->GetReflectionType()->GetName().ToLower();
 
-		if (hasPermission && permissionExpr) {
-			ScriptFrame permissionFrame(false, new Namespace());
-			// Sandboxing is lifted because this only evaluates the function from the
-			// ApiUser->permissions->filter
-			permissionFrame.Sandboxed = false;
-			return FilterUtility::EvaluateFilter(permissionFrame, permissionExpr.get(), obj, varName);
-		}
+	return CheckPermissionAndEvalFilter(perm, obj, varName);
+}
+
+bool FilterExprPermissionChecker::CheckPermissionAndEvalFilter(const String& permissionString, const Object::Ptr& obj, const String& varName)
+{
+	auto [it, inserted] = m_PermCache.try_emplace(permissionString);
+	auto& [hasPermission, permissionExpr] = it->second;
 
-		return hasPermission;
+	if (inserted) {
+		hasPermission = FilterUtility::HasPermission(m_User, permissionString, &permissionExpr);
 	}
 
-	std::unordered_map<String, std::pair<bool, std::unique_ptr<Expression>>> m_PermCache;
-	ApiUser::Ptr m_User;
-};
+	if (hasPermission && permissionExpr) {
+		ScriptFrame permissionFrame(false, new Namespace());
+		// Sandboxing is lifted because this only evaluates the function from the
+		// ApiUser->permissions->filter
+		permissionFrame.Sandboxed = false;
+		return FilterUtility::EvaluateFilter(permissionFrame, permissionExpr.get(), obj, varName);
+	}
+
+	return hasPermission;
+}
 
 Type::Ptr FilterUtility::TypeFromPluralName(const String& pluralName)
 {


=====================================
lib/remote/filterutility.hpp
=====================================
@@ -75,6 +75,31 @@ class MissingPermissionError : public ScriptError
 	using ScriptError::ScriptError;
 };
 
+/**
+ * Controls access to an object or variable based on an ApiUser's permissions.
+ *
+ * This is accomplished by caching the generated filter expressions so they don't have to be
+ * regenerated again and again when access is repeatedly checked in script functions and when
+ * evaluating expressions.
+ */
+class FilterExprPermissionChecker : public ScriptPermissionChecker
+{
+public:
+	DECLARE_PTR_TYPEDEFS(FilterExprPermissionChecker);
+
+	explicit FilterExprPermissionChecker(ApiUser::Ptr user);
+
+	Expression* CheckPermission(const String& permissionString);
+	bool CanAccessGlobalVariable(const String& varName) override;
+	bool CanAccessConfigObject(const ConfigObject::Ptr& obj) override;
+
+private:
+	bool CheckPermissionAndEvalFilter(const String& permissionString, const Object::Ptr& obj, const String& varName);
+
+	std::unordered_map<String, std::pair<bool, std::unique_ptr<Expression>>> m_PermCache;
+	ApiUser::Ptr m_User;
+};
+
 }
 
 #endif /* FILTERUTILITY_H */


=====================================
lib/remote/jsonrpc.hpp
=====================================
@@ -26,8 +26,8 @@ public:
 	static size_t SendMessage(const Shared<AsioTlsStream>::Ptr& stream, const Dictionary::Ptr& message, boost::asio::yield_context yc);
 	static size_t SendRawMessage(const Shared<AsioTlsStream>::Ptr& stream, const String& json, boost::asio::yield_context yc);
 
-	static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, ssize_t maxMessageLength = -1);
-	static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength = -1);
+	static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, ssize_t maxMessageLength);
+	static String ReadMessage(const Shared<AsioTlsStream>::Ptr& stream, boost::asio::yield_context yc, ssize_t maxMessageLength);
 
 	static Dictionary::Ptr DecodeMessage(const String& message);
 


=====================================
lib/remote/jsonrpcconnection-pki.cpp
=====================================
@@ -226,13 +226,9 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
 
 		String realTicket = PBKDF2_SHA1(cn, salt, 50000);
 
-		Log(LogDebug, "JsonRpcConnection")
-			<< "Certificate request for CN '" << cn << "': Comparing received ticket '"
-			<< ticket << "' with calculated ticket '" << realTicket << "'.";
-
 		if (!Utility::ComparePasswords(ticket, realTicket)) {
 			Log(LogWarning, "JsonRpcConnection")
-				<< "Ticket '" << ticket << "' for CN '" << cn << "' is invalid.";
+				<< "Received ticket for CN '" << cn << "' is invalid.";
 
 			result->Set("status_code", 1);
 			result->Set("error", "Invalid ticket for CN '" + cn + "'.");


=====================================
lib/remote/jsonrpcconnection.cpp
=====================================
@@ -73,7 +73,10 @@ void JsonRpcConnection::HandleIncomingMessages(boost::asio::yield_context yc)
 		String jsonString;
 
 		try {
-			jsonString = JsonRpc::ReadMessage(m_Stream, yc, m_Endpoint ? -1 : 1024 * 1024);
+			// Unauthenticated connections are limited to 1MB messages
+			jsonString = JsonRpc::ReadMessage(
+				m_Stream, yc, m_Endpoint ? m_Endpoint->GetMessageReceiveSizeLimit() : 1024L * 1024
+			);
 		} catch (const std::exception& ex) {
 			Log(m_ShuttingDown ? LogDebug : LogNotice, "JsonRpcConnection")
 				<< "Error while reading JSON-RPC message for identity '" << m_Identity


=====================================
lib/remote/pkiutility.cpp
=====================================
@@ -215,7 +215,8 @@ int PkiUtility::RequestCertificate(const String& host, const String& port, const
 		stream->flush();
 
 		for (;;) {
-			response = JsonRpc::DecodeMessage(JsonRpc::ReadMessage(stream));
+			// Parent nodes are trusted to send messages of unbounded size
+			response = JsonRpc::DecodeMessage(JsonRpc::ReadMessage(stream, -1));
 
 			if (response && response->Contains("error")) {
 				Log(LogCritical, "cli", "Could not fetch valid response. Please check the master log (notice or debug).");



View it on GitLab: https://salsa.debian.org/nagios-team/icinga2/-/compare/ab28d6dc9dd4040f3b3bd819d495e5e13c557fb4...158318e252f0046a6e1c7c2b6437c01c12bbaac6

-- 
View it on GitLab: https://salsa.debian.org/nagios-team/icinga2/-/compare/ab28d6dc9dd4040f3b3bd819d495e5e13c557fb4...158318e252f0046a6e1c7c2b6437c01c12bbaac6
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-nagios-changes/attachments/20260818/0d412576/attachment-0001.htm>


More information about the pkg-nagios-changes mailing list