s3token: Pass service auth token to Keystone

Recent versions of Keystone require auth tokens when accessing the
/v3/s3tokens endpoint to prevent exposure of a lot of information that
a user who just has a presigned URL should not be able to see.

UpgradeImpact
=============
The s3token middleware now requires Keystone auth credentials to be
configured. If secret_cache_duration is enabled, these credentials
should already be configured. Without these credentials, Keystone users
will no longer be able to make S3 API requests.

Closes-Bug: #2119646
Change-Id: Ie80bc33d0d9de17ca6eaad3b43628724538001f6
Signed-off-by: Tim Burke <tim.burke@gmail.com>
This commit is contained in:
Tim Burke
2025-10-31 09:11:39 -07:00
committed by Matthew Oliver
parent d87ebd7d05
commit e7bb2a3855
3 changed files with 67 additions and 36 deletions

View File

@@ -785,17 +785,6 @@ auth_uri = http://keystonehost:5000/v3
# Connect/read timeout (in seconds) to use when communicating with Keystone
http_timeout = 10.0
# Number of seconds to cache the S3 secret. By setting this to a positive
# number, the S3 authorization validation checks can happen locally.
# secret_cache_duration = 0
# If S3 secret caching is enabled, Keystone auth credentials to be used to
# validate S3 authorization must be provided here. The appropriate options
# are the same as used in the authtoken middleware above. The values are
# likely the same as used in the authtoken middleware.
# Note that the Keystone auth credentials used by s3token will need to be
# able to view all project credentials too.
# SSL-related options
# insecure = False
# certfile =
@@ -804,12 +793,10 @@ http_timeout = 10.0
# You can override the default log routing for this filter here:
# log_name = s3token
# Secrets may be cached to reduce latency for the client and load on Keystone.
# Set this to some number of seconds greater than zero to enable caching.
# secret_cache_duration = 0
# Secret caching requires Keystone credentials similar to the authtoken middleware;
# these credentials require access to view all project credentials.
# Recent Keystone deployments require credentials similar to the authtoken
# middleware; these credentials require access to the s3tokens endpoint.
# Additionally, if secret caching is enabled, the credentials should have
# access to view all project credentials.
# auth_url = http://keystonehost:5000
# auth_type = password
# project_domain_id = default
@@ -818,6 +805,11 @@ http_timeout = 10.0
# username = swift
# password = password
# Secrets may be cached to reduce latency for the client and load on Keystone.
# Set this to some number of seconds greater than zero to enable caching and
# allow some S3 authorization validation checks to happen entirely in the proxy.
# secret_cache_duration = 0
[filter:healthcheck]
use = egg:swift#healthcheck
# An optional filesystem path, which if present, will cause the healthcheck

View File

@@ -180,31 +180,45 @@ class S3Token(object):
self._secret_cache_duration = int(conf.get('secret_cache_duration', 0))
if self._secret_cache_duration < 0:
raise ValueError('secret_cache_duration must be non-negative')
if self._secret_cache_duration:
try:
auth_plugin = keystone_loading.get_plugin_loader(
conf.get('auth_type', 'password'))
available_auth_options = auth_plugin.get_options()
auth_options = {}
for option in available_auth_options:
name = option.name.replace('-', '_')
value = conf.get(name)
if value:
auth_options[name] = value
# Service authentication for s3tokens API calls
self.keystoneclient = None
try:
auth_plugin = keystone_loading.get_plugin_loader(
conf.get('auth_type', 'password'))
available_auth_options = auth_plugin.get_options()
auth_options = {}
for option in available_auth_options:
name = option.name.replace('-', '_')
value = conf.get(name)
if value:
auth_options[name] = value
if not auth_options:
self._logger.warning(
"No service auth configuration. "
"s3tokens API calls will be unauthenticated. "
"New versions of keystone require service auth.")
else:
auth = auth_plugin.load_from_options(**auth_options)
session = keystone_session.Session(auth=auth)
self.keystoneclient = keystone_client.Client(
session=session,
region_name=conf.get('region_name'))
self._logger.info("Caching s3tokens for %s seconds",
self._secret_cache_duration)
except Exception:
self._logger.warning("Unable to load keystone auth_plugin. "
"Secret caching will be unavailable.",
exc_info=True)
self.keystoneclient = None
self._secret_cache_duration = 0
self._logger.info(
"Service authentication configured for s3tokens API")
except Exception:
self._logger.warning(
"Unable to load service auth configuration. "
"s3tokens API calls will be unauthenticated "
"and secret caching will be unavailable.",
exc_info=True)
if self._secret_cache_duration and self.keystoneclient:
self._logger.info("Caching s3tokens for %s seconds",
self._secret_cache_duration)
else:
self._secret_cache_duration = 0
def _deny_request(self, code):
error_cls, message = {
@@ -222,6 +236,16 @@ class S3Token(object):
def _json_request(self, creds_json):
headers = {'Content-Type': 'application/json'}
# Add service authentication headers if configured
if self.keystoneclient:
try:
headers.update(
self.keystoneclient.session.get_auth_headers())
except Exception:
self._logger.warning("Failed to get service token",
exc_info=True)
try:
response = requests.post(self._request_uri,
headers=headers, data=creds_json,

View File

@@ -589,6 +589,9 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
cache.get.return_value = None
keystone_client = MOCK_KEYSTONE.return_value
keystone_client.session.get_auth_headers.return_value = {
'X-Auth-Token': 'bearer token',
}
keystone_client.ec2.get.return_value = mock.Mock(secret='secret')
MOCK_REQUEST.return_value = FakeResponse({
@@ -615,6 +618,18 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
}
self.assertTrue(MOCK_REQUEST.called)
self.assertEqual(MOCK_REQUEST.mock_calls, [
mock.call('http://example.com/s3tokens', headers={
'Content-Type': 'application/json',
'X-Auth-Token': 'bearer token',
}, data=json.dumps({
"credentials": {
"access": "access",
"token": "dG9rZW4=",
"signature": "signature",
}
}), verify=None, timeout=10.0)
])
tenant = GOOD_RESPONSE_V2['access']['token']['tenant']
expected_cache = (expected_headers, tenant, 'secret')
cache.set.assert_called_once_with('s3secret/access', expected_cache,