Version in base suite: 27.0.0-3 Base version: keystone_27.0.0-3 Target version: keystone_27.0.0-3+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/k/keystone/keystone_27.0.0-3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/k/keystone/keystone_27.0.0-3+deb13u1.dsc changelog | 15 + patches/keystone-bug-2119646-stable-2025.1.patch | 315 +++++++++++++++++++++++ patches/series | 1 3 files changed, 331 insertions(+) diff -Nru keystone-27.0.0/debian/changelog keystone-27.0.0/debian/changelog --- keystone-27.0.0/debian/changelog 2025-07-11 12:00:40.000000000 +0000 +++ keystone-27.0.0/debian/changelog 2025-10-30 08:26:19.000000000 +0000 @@ -1,3 +1,18 @@ +keystone (2:27.0.0-3+deb13u1) trixie-security; urgency=high + + * OSSA-2025-002: kay reported a vulnerability in Keystone’s ec2tokens and + s3tokens APIs. By sending those endpoints a valid AWS Signature (e.g., from + a presigned S3 URL), an unauthenticated attacker may obtain Keystone + authorization (ec2tokens can yield a fully scoped token; s3tokens can + reveal scope accepted by some services), resulting in unauthorized access + and privilege escalation. Deployments where /v3/ec2tokens or /v3/s3tokens + are reachable by unauthenticated clients (e.g., exposed on a public API) + are affected. + Applied upstream patch (Closes: #1120053): + - keystone-bug-2119646-stable-2025.1.patch + + -- Thomas Goirand Thu, 30 Oct 2025 09:26:19 +0100 + keystone (2:27.0.0-3) unstable; urgency=medium * export OS_OSLO_MESSAGING_RABBIT__PROCESSNAME. diff -Nru keystone-27.0.0/debian/patches/keystone-bug-2119646-stable-2025.1.patch keystone-27.0.0/debian/patches/keystone-bug-2119646-stable-2025.1.patch --- keystone-27.0.0/debian/patches/keystone-bug-2119646-stable-2025.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ keystone-27.0.0/debian/patches/keystone-bug-2119646-stable-2025.1.patch 2025-10-30 08:26:19.000000000 +0000 @@ -0,0 +1,315 @@ +Author: Grzegorz Grasza +Date: Fri, 19 Sep 2025 14:02:18 +0200 +Description: Add service user authentication to ec2 and s3 endpoints + Add a policy to enforce authentication with a user in the service + group. This maintains AWS compatibility with the added security + layer. +Bug: https://bugs.launchpad.net/keystone/+bug/2119646 +Change-Id: Ic84b84247e05f29874e2c5636a033aaedd4de83c +Signed-off-by: Jeremy Stanley +Origin: https://bugs.launchpad.net/keystone/+bug/2119646 +Last-Update: 2025-10-30 + +Index: keystone/keystone/api/ec2tokens.py +=================================================================== +--- keystone.orig/keystone/api/ec2tokens.py ++++ keystone/keystone/api/ec2tokens.py +@@ -21,6 +21,7 @@ from oslo_serialization import jsonutils + + from keystone.api._shared import EC2_S3_Resource + from keystone.api._shared import json_home_relations ++from keystone.common import rbac_enforcer + from keystone.common import render_token + from keystone.common import utils + from keystone import exception +@@ -30,6 +31,9 @@ from keystone.server import flask as ks_ + CRED_TYPE_EC2 = 'ec2' + + ++ENFORCER = rbac_enforcer.RBACEnforcer ++ ++ + class EC2TokensResource(EC2_S3_Resource.ResourceBase): + @staticmethod + def _check_signature(creds_ref, credentials): +@@ -57,12 +61,14 @@ class EC2TokensResource(EC2_S3_Resource. + else: + raise exception.Unauthorized(_('EC2 signature not supplied.')) + +- @ks_flask.unenforced_api + def post(self): + """Authenticate ec2 token. + + POST /v3/ec2tokens + """ ++ # Enforce RBAC in the same way as S3 tokens ++ ENFORCER.enforce_call(action='identity:ec2tokens_validate') ++ + token = self.handle_authenticate() + token_reference = render_token.render_token_response_from_model(token) + resp_body = jsonutils.dumps(token_reference) +Index: keystone/keystone/api/s3tokens.py +=================================================================== +--- keystone.orig/keystone/api/s3tokens.py ++++ keystone/keystone/api/s3tokens.py +@@ -22,12 +22,15 @@ from oslo_serialization import jsonutils + + from keystone.api._shared import EC2_S3_Resource + from keystone.api._shared import json_home_relations ++from keystone.common import rbac_enforcer + from keystone.common import render_token + from keystone.common import utils + from keystone import exception + from keystone.i18n import _ + from keystone.server import flask as ks_flask + ++ENFORCER = rbac_enforcer.RBACEnforcer ++ + + def _calculate_signature_v1(string_to_sign, secret_key): + """Calculate a v1 signature. +@@ -96,12 +99,14 @@ class S3Resource(EC2_S3_Resource.Resourc + message=_('Credential signature mismatch') + ) + +- @ks_flask.unenforced_api + def post(self): + """Authenticate s3token. + + POST /v3/s3tokens + """ ++ # Use standard Keystone policy enforcement for s3tokens access ++ ENFORCER.enforce_call(action='identity:s3tokens_validate') ++ + token = self.handle_authenticate() + token_reference = render_token.render_token_response_from_model(token) + resp_body = jsonutils.dumps(token_reference) +Index: keystone/keystone/common/policies/__init__.py +=================================================================== +--- keystone.orig/keystone/common/policies/__init__.py ++++ keystone/keystone/common/policies/__init__.py +@@ -22,6 +22,7 @@ from keystone.common.policies import cre + from keystone.common.policies import domain + from keystone.common.policies import domain_config + from keystone.common.policies import ec2_credential ++from keystone.common.policies import ec2tokens + from keystone.common.policies import endpoint + from keystone.common.policies import endpoint_group + from keystone.common.policies import grant +@@ -40,6 +41,7 @@ from keystone.common.policies import reg + from keystone.common.policies import revoke_event + from keystone.common.policies import role + from keystone.common.policies import role_assignment ++from keystone.common.policies import s3tokens + from keystone.common.policies import service + from keystone.common.policies import service_provider + from keystone.common.policies import token +@@ -78,6 +80,8 @@ def list_rules(): + revoke_event.list_rules(), + role.list_rules(), + role_assignment.list_rules(), ++ s3tokens.list_rules(), ++ ec2tokens.list_rules(), + service.list_rules(), + service_provider.list_rules(), + token_revocation.list_rules(), +Index: keystone/keystone/common/policies/ec2tokens.py +=================================================================== +--- /dev/null ++++ keystone/keystone/common/policies/ec2tokens.py +@@ -0,0 +1,37 @@ ++# 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 keystone.common.policies import base ++ ++# Align EC2 tokens API with S3 tokens: require admin or service users ++ADMIN_OR_SERVICE = 'rule:service_or_admin' ++ ++ ++ec2tokens_policies = [ ++ policy.DocumentedRuleDefault( ++ name=base.IDENTITY % 'ec2tokens_validate', ++ check_str=ADMIN_OR_SERVICE, ++ scope_types=['system', 'domain', 'project'], ++ description='Validate EC2 credentials and create a Keystone token. ' ++ 'Restricted to service users or administrators.', ++ operations=[{'path': '/v3/ec2tokens', 'method': 'POST'}], ++ ) ++] ++ ++ ++def list_rules(): ++ return ec2tokens_policies ++ ++ ++ +Index: keystone/keystone/common/policies/s3tokens.py +=================================================================== +--- /dev/null ++++ keystone/keystone/common/policies/s3tokens.py +@@ -0,0 +1,35 @@ ++# 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 keystone.common.policies import base ++ ++# S3 tokens API requires service authentication to prevent presigned URL exploitation ++# This policy restricts access to service users or administrators only ++ADMIN_OR_SERVICE = 'rule:service_or_admin' ++ ++s3tokens_policies = [ ++ policy.DocumentedRuleDefault( ++ name=base.IDENTITY % 's3tokens_validate', ++ check_str=ADMIN_OR_SERVICE, ++ scope_types=['system', 'domain', 'project'], ++ description='Validate S3 credentials and create a Keystone token. ' ++ 'Restricted to service users or administrators to prevent ' ++ 'exploitation via presigned URLs.', ++ operations=[{'path': '/v3/s3tokens', 'method': 'POST'}], ++ ) ++] ++ ++ ++def list_rules(): ++ return s3tokens_policies +Index: keystone/keystone/tests/unit/test_contrib_ec2_core.py +=================================================================== +--- keystone.orig/keystone/tests/unit/test_contrib_ec2_core.py ++++ keystone/keystone/tests/unit/test_contrib_ec2_core.py +@@ -62,20 +62,34 @@ class EC2ContribCoreV3(test_v3.RestfulTe + }, + } + credentials['signature'] = signer.generate(credentials) ++ # Authenticate as system admin by default unless overridden via kwargs ++ token = None ++ if 'noauth' in kwargs and kwargs['noauth']: ++ token = None ++ else: ++ PROVIDERS.assignment_api.create_system_grant_for_user( ++ self.user_id, self.role_id ++ ) ++ token = self.get_system_scoped_token() ++ ++ expected_status = kwargs.get('expected_status', http.client.OK) + resp = self.post( + '/ec2tokens', + body={'credentials': credentials}, +- expected_status=http.client.OK, +- **kwargs, ++ expected_status=expected_status, ++ token=token, ++ noauth=kwargs.get('noauth'), + ) +- self.assertValidProjectScopedTokenResponse(resp, self.user) ++ if expected_status == http.client.OK: ++ self.assertValidProjectScopedTokenResponse(resp, self.user) + + def test_valid_authentication_response_with_proper_secret(self): + self._test_valid_authentication_response_with_proper_secret() + + def test_valid_authentication_response_with_proper_secret_noauth(self): ++ # ec2 endpoint now enforces RBAC; unauthenticated should be denied + self._test_valid_authentication_response_with_proper_secret( +- noauth=True ++ expected_status=http.client.UNAUTHORIZED, noauth=True + ) + + def test_valid_authentication_response_with_signature_v4(self): +Index: keystone/keystone/tests/unit/test_contrib_s3_core.py +=================================================================== +--- keystone.orig/keystone/tests/unit/test_contrib_s3_core.py ++++ keystone/keystone/tests/unit/test_contrib_s3_core.py +@@ -55,7 +55,7 @@ class S3ContribCore(test_v3.RestfulTestC + ) + self.assertEqual(http.client.METHOD_NOT_ALLOWED, resp.status_code) + +- def _test_good_response(self, **kwargs): ++ def _test_good_response(self, expected_status=http.client.OK, **kwargs): + sts = 'string to sign' # opaque string from swift3 + sig = hmac.new( + self.cred_blob['secret'].encode('ascii'), +@@ -71,18 +71,22 @@ class S3ContribCore(test_v3.RestfulTestC + 'token': base64.b64encode(sts.encode('ascii')).strip(), + } + }, +- expected_status=http.client.OK, ++ expected_status=expected_status, + **kwargs, + ) +- self.assertValidProjectScopedTokenResponse( +- resp, self.user, forbid_token_id=True +- ) ++ if expected_status == http.client.OK: ++ self.assertValidProjectScopedTokenResponse( ++ resp, self.user, forbid_token_id=True ++ ) ++ else: ++ self.assertValidErrorResponse(resp) + + def test_good_response(self): + self._test_good_response() + + def test_good_response_noauth(self): +- self._test_good_response(noauth=True) ++ # s3tokens now requires service/admin auth; unauthenticated should be denied ++ self._test_good_response(http.client.UNAUTHORIZED, noauth=True) + + def test_bad_request(self): + self.post( +Index: keystone/keystone/tests/unit/test_v3_credential.py +=================================================================== +--- keystone.orig/keystone/tests/unit/test_v3_credential.py ++++ keystone/keystone/tests/unit/test_v3_credential.py +@@ -90,10 +90,15 @@ class CredentialBaseTestCase(test_v3.Res + 'path': '/bar', + 'params': params, + } ++ PROVIDERS.assignment_api.create_system_grant_for_user( ++ self.user_id, self.role_id ++ ) ++ token = self.get_system_scoped_token() + r = self.post( + '/ec2tokens', + body={'ec2Credentials': sig_ref}, + expected_status=http.client.OK, ++ token=token, + ) + self.assertValidTokenResponse(r) + return r.result['token'] +Index: keystone/doc/source/getting-started/policy_mapping.rst +=================================================================== +--- keystone.orig/doc/source/getting-started/policy_mapping.rst ++++ keystone/doc/source/getting-started/policy_mapping.rst +@@ -246,6 +246,9 @@ identity:get_access_rule + identity:list_access_rules GET /v3/users/{user_id}/access_rules + identity:delete_access_rule DELETE /v3/users/{user_id}/access_rules/{access_rule_id} + ++identity:ec2tokens_validate POST /v3/ec2tokens ++identity:s3tokens_validate POST /v3/s3tokens ++ + ========================================================= === + + .. _grant_resources: diff -Nru keystone-27.0.0/debian/patches/series keystone-27.0.0/debian/patches/series --- keystone-27.0.0/debian/patches/series 2025-07-11 12:00:40.000000000 +0000 +++ keystone-27.0.0/debian/patches/series 2025-10-30 08:26:19.000000000 +0000 @@ -2,3 +2,4 @@ do-not-set-chartset-in-flask-responce.patch set-deprecation-warnings-to-ignore.patch api_Remove_constraints_on_user_IDs.patch +keystone-bug-2119646-stable-2025.1.patch