Bug#1144930: trixie-pu: package watcher/14.0.0-1+deb13u1
Thomas Goirand <[email protected]>
| Newsgroups | gmane.linux.debian.devel.release |
|---|---|
| Message-ID | <178723986505.240050.9113910141442671949.reportbug__13118.5497551755$1787240018$gmane$org@zbuz.infomaniak.ch> |
Package: release.debian.org Severity: normal Tags: trixie X-Debbugs-Cc: [email protected] Control: affects -1 + src:watcher User: [email protected] Usertags: pu Hi, [ Reason ] I would like to upload watcher watcher/14.0.0-1+deb13u2 to address this upstream issue: https://security.openstack.org/ossa/OSSA-2026-036.html aka: https://bugs.debian.org/1144880 [ Impact ] As per upstream announce: Watcher does not apply authorization to its webhook trigger endpoint. Any authenticated user who learns an audit’s webhook URL, for example from the Aodh alarm metadata leaked above, can start an EVENT audit and its associated action plan regardless of their own project or role. [ Tests ] Upstream patch includes new tests, also run at package build time. I do not run Watcher myself in production, though upstream also runs functional testing to validate new patches. [ Risks ] Patch is not very big. [ Checklist ] [x] *all* changes are documented in the d/changelog [x] I reviewed all changes and I approve them [x] attach debdiff against the package in (old)stable [x] the issue is verified as fixed in unstable [ Changes ] (Explain *all* the changes) Please allow me to upload watcher/14.0.0-1+deb13u2. Cheers, Thomas Goirand (zigo)
watcher_14.0.0-1+deb13u2.debdiff
(text/plain, 14.4 KB)
diff -Nru watcher-14.0.0/debian/changelog watcher-14.0.0/debian/changelog --- watcher-14.0.0/debian/changelog 2025-07-11 14:45:24.000000000 +0200 +++ watcher-14.0.0/debian/changelog 2026-08-20 14:36:03.000000000 +0200 @@ -1,3 +1,17 @@ +watcher (14.0.0-1+deb13u2) trixie; urgency=medium + + * CVE-2026-XXXXX / OSSA-2026-036: Watcher webhook trigger endpoint does not + enforce oslo.policy authorization. Any authenticated user who learns a + Watcher audit webhook URL can POST to the webhook to trigger an + administrator-owned EVENT audit and its associated action plan, regardless + of the caller's project or role. The webhook endpoint has lacked policy + enforcement since its introduction in the Ussuri release. Applied upstream + patch: "Add policy enforcement to webhook trigger endpoint". + (Closes: #1144880) + * Added python3-ddt as build-depends, needed for this patch. + + -- Thomas Goirand <[email protected]> Thu, 20 Aug 2026 14:36:03 +0200 + watcher (14.0.0-1+deb13u1) trixie; urgency=medium * Add export OS_OSLO_MESSAGING_RABBIT__PROCESSNAME for all daemons. diff -Nru watcher-14.0.0/debian/control watcher-14.0.0/debian/control --- watcher-14.0.0/debian/control 2025-07-11 14:45:24.000000000 +0200 +++ watcher-14.0.0/debian/control 2026-08-20 14:36:03.000000000 +0200 @@ -19,6 +19,7 @@ python3-cinderclient, python3-coverage, python3-croniter, + python3-ddt, python3-eventlet, python3-freezegun, python3-futurist, diff -Nru watcher-14.0.0/debian/patches/CVE-2026-XXXXX-watcher-lp-2161771-2025.1.patch watcher-14.0.0/debian/patches/CVE-2026-XXXXX-watcher-lp-2161771-2025.1.patch --- watcher-14.0.0/debian/patches/CVE-2026-XXXXX-watcher-lp-2161771-2025.1.patch 1970-01-01 01:00:00.000000000 +0100 +++ watcher-14.0.0/debian/patches/CVE-2026-XXXXX-watcher-lp-2161771-2025.1.patch 2026-08-20 14:36:03.000000000 +0200 @@ -0,0 +1,324 @@ +Description: Add policy enforcement to webhook trigger endpoint + The webhook POST endpoint (/v1/webhooks/{audit_uuid}) was entirely + unprotected even when [api] enable_webhooks_auth was set to True, as no + oslo_policy check was applied to the request. + . + This patch introduce a new webhook policy module with a webhook:trigger rule + backed by the new admin_or_service_api base rule (role:admin, + role:administrator, or role:service). The policy is enforced in the + WebhookController.post() method when enable_webhooks_auth is enabled, + rejecting callers that do not hold one of the required roles with HTTP 403. + When enable_webhooks_auth is False the endpoint remains unauthenticated and + no policy check is applied. + . +Author: Douglas Viroel <[email protected]> +Date: Tue, 28 Jul 2026 15:19:55 -0300 +Bug: https://bugs.launchpad.net/bugs/2161771 +Bug-Debian: https://bugs.debian.org/1144880 +Assisted-By: Claude Code (Sonnet 4.6) +Change-Id: Ic56f82c6825f0dc8e83ee92a92e3c3479404252d +Signed-off-by: Douglas Viroel <[email protected]> +Origin: upstream, https://bugs.launchpad.net/ossa/+bug/2161771 +Last-Update: 2026-08-13 + +diff --git a/releasenotes/notes/bug-2161771-add-policy-enforcement-event-audit-596288dbff87bd0d.yaml b/releasenotes/notes/bug-2161771-add-policy-enforcement-event-audit-596288dbff87bd0d.yaml +new file mode 100644 +index 00000000..57c3b6e9 +--- /dev/null ++++ b/releasenotes/notes/bug-2161771-add-policy-enforcement-event-audit-596288dbff87bd0d.yaml +@@ -0,0 +1,12 @@ ++--- ++security: ++ - | ++ Adds policy enforcement to the webhook trigger endpoint ++ (``POST /v1/webhooks/{audit_uuid}``). When ``[api] enable_webhooks_auth`` ++ is ``True`` (the default), the new ``webhook:trigger`` policy rule is ++ enforced, requiring the caller to hold the ``admin``, ``administrator``, ++ or ``service`` role. Requests that do not satisfy the rule are rejected ++ with HTTP 403. When ``enable_webhooks_auth`` is ``False`` the endpoint ++ remains unauthenticated and no policy check is applied. For more details, ++ please see ++ `Bug #2161771 <https://bugs.launchpad.net/watcher/+bug/2161771>`_. +diff --git a/test-requirements.txt b/test-requirements.txt +index 4b261e60..4d191d61 100644 +--- a/test-requirements.txt ++++ b/test-requirements.txt +@@ -1,4 +1,5 @@ + coverage>=4.5.1 # Apache-2.0 ++ddt>=1.2.1 # MIT + freezegun>=0.3.10 # Apache-2.0 + oslotest>=3.3.0 # Apache-2.0 + testscenarios>=0.5.0 # Apache-2.0/BSD +diff --git a/watcher/api/controllers/v1/webhooks.py b/watcher/api/controllers/v1/webhooks.py +index 61ae40a0..7ec19545 100644 +--- a/watcher/api/controllers/v1/webhooks.py ++++ b/watcher/api/controllers/v1/webhooks.py +@@ -15,6 +15,7 @@ Webhook endpoint for Watcher v1 REST API. + """ + + from http import HTTPStatus ++from oslo_config import cfg + from oslo_log import log + import pecan + from pecan import rest +@@ -24,9 +25,11 @@ import wsmeext.pecan as wsme_pecan + from watcher.api.controllers.v1 import types + from watcher.api.controllers.v1 import utils + from watcher.common import exception ++from watcher.common import policy + from watcher.decision_engine import rpcapi + from watcher import objects + ++CONF = cfg.CONF + LOG = log.getLogger(__name__) + + +@@ -45,9 +48,15 @@ class WebhookController(rest.RestController): + :param audit_ident: UUID or name of an audit. + """ + ++ context = pecan.request.context ++ ++ if CONF.api.enable_webhooks_auth: ++ policy.enforce( ++ context, 'webhook:trigger', action='webhook:trigger' ++ ) ++ + LOG.debug("Webhook trigger Audit: %s.", audit_ident) + +- context = pecan.request.context + audit = utils.get_resource('Audit', audit_ident) + if audit is None: + raise exception.AuditNotFound(audit=audit_ident) +diff --git a/watcher/common/policies/__init__.py b/watcher/common/policies/__init__.py +index b773f569..47d8f345 100644 +--- a/watcher/common/policies/__init__.py ++++ b/watcher/common/policies/__init__.py +@@ -22,6 +22,7 @@ from watcher.common.policies import goal + from watcher.common.policies import scoring_engine + from watcher.common.policies import service + from watcher.common.policies import strategy ++from watcher.common.policies import webhooks + + + def list_rules(): +@@ -36,4 +37,5 @@ def list_rules(): + scoring_engine.list_rules(), + service.list_rules(), + strategy.list_rules(), ++ webhooks.list_rules(), + ) +diff --git a/watcher/common/policies/base.py b/watcher/common/policies/base.py +index 6e5dec7f..6f5b5b91 100644 +--- a/watcher/common/policies/base.py ++++ b/watcher/common/policies/base.py +@@ -13,6 +13,7 @@ + from oslo_policy import policy + + RULE_ADMIN_API = 'rule:admin_api' ++RULE_ADMIN_OR_SERVICE = 'rule:admin_or_service_api' + ROLE_ADMIN_OR_ADMINISTRATOR = 'role:admin or role:administrator' + ALWAYS_DENY = '!' + +@@ -21,6 +22,11 @@ rules = [ + name='admin_api', + check_str=ROLE_ADMIN_OR_ADMINISTRATOR + ), ++ policy.RuleDefault( ++ name='admin_or_service_api', ++ check_str='rule:admin_api or role:service', ++ description='Default rule for service or admin APIs.', ++ ), + policy.RuleDefault( + name='show_password', + check_str=ALWAYS_DENY +diff --git a/watcher/common/policies/webhooks.py b/watcher/common/policies/webhooks.py +new file mode 100644 +index 00000000..2e79c39a +--- /dev/null ++++ b/watcher/common/policies/webhooks.py +@@ -0,0 +1,32 @@ ++# Licensed under the Apache License, Version 2.0 (the "License"); you may ++# not use this file except in compliance with the License. You may obtain ++# a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT ++# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the ++# License for the specific language governing permissions and limitations ++# under the License. ++ ++from oslo_policy import policy ++ ++from watcher.common.policies import base ++ ++ ++WEBHOOK = 'webhook:%s' ++ ++rules = [ ++ policy.DocumentedRuleDefault( ++ name=WEBHOOK % 'trigger', ++ check_str=base.RULE_ADMIN_OR_SERVICE, ++ description='Trigger an audit via webhook. Enforced only when ' ++ 'enable_webhooks_auth is enabled.', ++ operations=[{'path': '/v1/webhooks/{audit_uuid}', 'method': 'POST'}], ++ ) ++] ++ ++ ++def list_rules(): ++ return rules +diff --git a/watcher/tests/api/v1/test_webhooks.py b/watcher/tests/api/v1/test_webhooks.py +index bd20000e..695e6277 100644 +--- a/watcher/tests/api/v1/test_webhooks.py ++++ b/watcher/tests/api/v1/test_webhooks.py +@@ -14,12 +14,20 @@ from unittest import mock + + from http import HTTPStatus + ++import ddt ++ ++from oslo_config import cfg ++ ++from watcher.common import context as watcher_context + from watcher.decision_engine import rpcapi as deapi + from watcher import objects + from watcher.tests.api import base as api_base + from watcher.tests.objects import utils as obj_utils + + ++CONF = cfg.CONF ++ ++ + class TestPost(api_base.FunctionalTest): + + def setUp(self): +@@ -71,3 +79,105 @@ class TestPost(api_base.FunctionalTest): + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) ++ ++ [email protected] ++class TestWebhookPolicyEnforcement(api_base.FunctionalTest): ++ def setUp(self): ++ super().setUp() ++ obj_utils.create_test_goal(self.context) ++ obj_utils.create_test_strategy(self.context) ++ obj_utils.create_test_audit_template(self.context) ++ ++ def _create_event_audit(self): ++ return obj_utils.create_test_audit( ++ self.context, audit_type=objects.audit.AuditType.EVENT.value ++ ) ++ ++ def _set_admin_or_service_policy(self): ++ self.policy.set_rules( ++ { ++ 'admin_or_service_api': ( ++ 'role:admin or role:administrator or role:service' ++ ), ++ 'webhook:trigger': 'rule:admin_or_service_api', ++ } ++ ) ++ ++ def _make_context_with_roles(self, roles): ++ def make_context(*args, **kwargs): ++ kwargs.setdefault('project_id', 'fake_project') ++ kwargs.setdefault('user_id', 'fake_user') ++ kwargs['roles'] = roles ++ context = watcher_context.RequestContext(*args, **kwargs) ++ return watcher_context.RequestContext.from_dict(context.to_dict()) ++ ++ return make_context ++ ++ @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') ++ def test_trigger_policy_disallowed_without_admin_or_service_role( ++ self, mock_trigger_audit ++ ): ++ CONF.set_override('enable_webhooks_auth', True, group='api') ++ self._set_admin_or_service_policy() ++ audit = self._create_event_audit() ++ response = self.post_json( ++ '/webhooks/{}'.format(audit['uuid']), ++ {}, ++ headers={'OpenStack-API-Version': 'infra-optim 1.4'}, ++ expect_errors=True, ++ ) ++ self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int) ++ mock_trigger_audit.assert_not_called() ++ ++ @mock.patch.object(watcher_context, 'make_context') ++ @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') ++ @ddt.data("admin", "service") ++ def test_trigger_policy_allowed_with_role( ++ self, role, mock_trigger_audit, mock_make_context ++ ): ++ CONF.set_override('enable_webhooks_auth', True, group='api') ++ self._set_admin_or_service_policy() ++ mock_make_context.side_effect = self._make_context_with_roles([role]) ++ audit = self._create_event_audit() ++ response = self.post_json( ++ '/webhooks/{}'.format(audit['uuid']), ++ {}, ++ headers={'OpenStack-API-Version': 'infra-optim 1.4'}, ++ ) ++ self.assertEqual(HTTPStatus.ACCEPTED, response.status_int) ++ mock_trigger_audit.assert_called_once_with(mock.ANY, audit['uuid']) ++ ++ @mock.patch.object(watcher_context, 'make_context') ++ @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') ++ @ddt.data("member", "reader") ++ def test_trigger_policy_disallowed_with_role( ++ self, role, mock_trigger_audit, mock_make_context ++ ): ++ CONF.set_override('enable_webhooks_auth', True, group='api') ++ self._set_admin_or_service_policy() ++ mock_make_context.side_effect = self._make_context_with_roles([role]) ++ audit = self._create_event_audit() ++ response = self.post_json( ++ '/webhooks/{}'.format(audit['uuid']), ++ {}, ++ headers={'OpenStack-API-Version': 'infra-optim 1.4'}, ++ expect_errors=True, ++ ) ++ self.assertEqual(HTTPStatus.FORBIDDEN, response.status_int) ++ mock_trigger_audit.assert_not_called() ++ ++ @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') ++ def test_trigger_policy_not_enforced_when_auth_disabled( ++ self, mock_trigger_audit ++ ): ++ CONF.set_override('enable_webhooks_auth', False, group='api') ++ self._set_admin_or_service_policy() ++ audit = self._create_event_audit() ++ response = self.post_json( ++ '/webhooks/{}'.format(audit['uuid']), ++ {}, ++ headers={'OpenStack-API-Version': 'infra-optim 1.4'}, ++ ) ++ self.assertEqual(HTTPStatus.ACCEPTED, response.status_int) ++ mock_trigger_audit.assert_called_once_with(mock.ANY, audit['uuid']) +diff --git a/watcher/tests/fake_policy.py b/watcher/tests/fake_policy.py +index 4d493e09..02db85ae 100644 +--- a/watcher/tests/fake_policy.py ++++ b/watcher/tests/fake_policy.py +@@ -16,9 +16,13 @@ + policy_data = """ + { + "admin_api": "role:admin or role:administrator", ++ "service_api": "", ++ "admin_or_service_api": "", + "show_password": "!", + "default": "rule:admin_api", + ++ "webhook:trigger": "", ++ + "action:detail": "", + "action:get": "", + "action:get_all": "", +-- +2.54.0 + diff -Nru watcher-14.0.0/debian/patches/series watcher-14.0.0/debian/patches/series --- watcher-14.0.0/debian/patches/series 2025-07-11 14:45:24.000000000 +0200 +++ watcher-14.0.0/debian/patches/series 2026-08-20 14:36:03.000000000 +0200 @@ -2,3 +2,4 @@ remove-sphinxcontrib.rsvgconverter.patch removed-sphinxcontrib.httpdomain-from-sphinx-ext.patch OSSN-0094_use_cinder_migrate_for_swap_volume.patch +CVE-2026-XXXXX-watcher-lp-2161771-2025.1.patch