Version in base suite: 14.0.0-1+deb13u1 Base version: watcher_14.0.0-1+deb13u1 Target version: watcher_14.0.0-1+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/w/watcher/watcher_14.0.0-1+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/w/watcher/watcher_14.0.0-1+deb13u2.dsc changelog | 14 control | 1 patches/CVE-2026-76878-watcher-lp-2161771-2025.1.patch | 324 +++++++++++++++++ patches/series | 1 4 files changed, 340 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpzvppasvr/watcher_14.0.0-1+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpzvppasvr/watcher_14.0.0-1+deb13u2.dsc: no acceptable signature found diff -Nru watcher-14.0.0/debian/changelog watcher-14.0.0/debian/changelog --- watcher-14.0.0/debian/changelog 2025-07-11 12:45:24.000000000 +0000 +++ watcher-14.0.0/debian/changelog 2026-08-20 12:36:03.000000000 +0000 @@ -1,3 +1,17 @@ +watcher (14.0.0-1+deb13u2) trixie; urgency=medium + + * CVE-2026-76878 / 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 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 12:45:24.000000000 +0000 +++ watcher-14.0.0/debian/control 2026-08-20 12:36:03.000000000 +0000 @@ -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-76878-watcher-lp-2161771-2025.1.patch watcher-14.0.0/debian/patches/CVE-2026-76878-watcher-lp-2161771-2025.1.patch --- watcher-14.0.0/debian/patches/CVE-2026-76878-watcher-lp-2161771-2025.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ watcher-14.0.0/debian/patches/CVE-2026-76878-watcher-lp-2161771-2025.1.patch 2026-08-20 12:36:03.000000000 +0000 @@ -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 +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 +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 `_. +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']) ++ ++ ++@ddt.ddt ++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 12:45:24.000000000 +0000 +++ watcher-14.0.0/debian/patches/series 2026-08-20 12:36:03.000000000 +0000 @@ -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-76878-watcher-lp-2161771-2025.1.patch