From 34c71dadba01470d3dc6a2158b5dcab8ea2ab5fc Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Tue, 6 Feb 2024 07:50:55 +0000 Subject: [PATCH] rgw/auth: ignoring signatures for HTTP OPTIONS calls Before [1] we always sent all HTTP OPTIONS requests to the S3AnonymousEngine and ignored any provided AWSv4 credentials sent in the request. That PR changed so that if we got credentials in the request we instead sent it through the authentication code in order to solve HTTP OPTIONS requests on tenanted users to start working (because we need to resolve the tenant, also called bucket tenant in the code, and we can't only rely on the bucket name since it will not be found). We solved this by modifying the canonical HTTP method used when calculating the AWSv4 signature by instead using the access-control-request-method header which worked good. This change did not take into account that when you generated a presigned URL for a put_object request you can also pass in extra parameters like a canned ACL [2] to the Params variable in for example boto3's generated_presigned_url(). Doing that will cause the client to add the x-amz-acl header to x-amz-signedheaders and also use that in their signature calculation. When doing a HTTP OPTIONS calls for CORS on that presigned URL the browser will never send a x-amz-acl header with the correct data since that is something that the actual PUT request should include later, so that HTTP OPTIONS call should pass even though the signature can never be calculated correctly server-side like verified against AWS S3 in tracker [3]. This patch as a result skips the signature calculation when doing EC2 auth using the LocalEngine but we still need to pass the request there in order to lookup the user to support buckets in a tenant. For the Keystone EC2 auth we're pretty out of luck in the sense that Keystone's API itself requires us to send the AWSv4 signature in the request with the access_key in order to obtain a token, and we cannot leave the signature out, we also cannot spoof the signature from rgw -> keystone since we don't have access to the secret_key if it's not in our cache. For that approach we simply pass on to get_access_token() that if it's an HTTP OPTIONS and we find the access_key in the cache we pull that and ignore verifying signature and pass it on for validation. This means that the cache must be warm if using Keystone auth and adding extra params to a presigned URL. This partly makes some of the commits in [1] redundant for EC2 LocalEngine auth but we still need it for tenanted bucket support. [1] https://github.com/ceph/ceph/pull/52673 [2] https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl [3] https://tracker.ceph.com/issues/64308 Fixes: https://tracker.ceph.com/issues/64308 Signed-off-by: Tobias Urdin (cherry picked from commit fe15b52edb5d228d2ed56679c62cf48493ae2d54) Conflicts: missing 'rgw/keystone: use secret key from EC2 for sigv4 streaming mode' src/rgw/rgw_auth_keystone.cc src/rgw/rgw_auth_keystone.h --- src/rgw/rgw_auth_keystone.cc | 25 ++++++++++++++++++------- src/rgw/rgw_auth_keystone.h | 3 ++- src/rgw/rgw_rest_s3.cc | 7 +++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/rgw/rgw_auth_keystone.cc b/src/rgw/rgw_auth_keystone.cc index 9879d339f9cc4..fced30f7740b0 100644 --- a/src/rgw/rgw_auth_keystone.cc +++ b/src/rgw/rgw_auth_keystone.cc @@ -452,7 +452,8 @@ EC2Engine::get_access_token(const DoutPrefixProvider* dpp, const std::string_view& access_key_id, const std::string& string_to_sign, const std::string_view& signature, - const signature_factory_t& signature_factory) const + const signature_factory_t& signature_factory, + bool ignore_signature) const { using server_signature_t = VersionAbstractor::server_signature_t; boost::optional token; @@ -464,12 +465,19 @@ EC2Engine::get_access_token(const DoutPrefixProvider* dpp, /* Check that credentials can correctly be used to sign data */ if (t) { - std::string sig(signature); - server_signature_t server_signature = signature_factory(cct, t->get<1>(), string_to_sign); - if (sig.compare(server_signature) == 0) { + /* We should ignore checking signature in cache if caller tells us to which + * means we're handling a HTTP OPTIONS call. */ + if (ignore_signature) { + ldpp_dout(dpp, 20) << "ignore_signature set and found in cache" << dendl; return std::make_pair(t->get<0>(), 0); } else { - ldpp_dout(dpp, 0) << "Secret string does not correctly sign payload, cache miss" << dendl; + std::string sig(signature); + server_signature_t server_signature = signature_factory(cct, t->get<1>(), string_to_sign); + if (sig.compare(server_signature) == 0) { + return std::make_pair(t->get<0>(), 0); + } else { + ldpp_dout(dpp, 0) << "Secret string does not correctly sign payload, cache miss" << dendl; + } } } else { ldpp_dout(dpp, 0) << "No stored secret string, cache miss" << dendl; @@ -541,7 +549,6 @@ rgw::auth::Engine::result_t EC2Engine::authenticate( const string_to_sign_t& string_to_sign, const signature_factory_t& signature_factory, const completer_factory_t& completer_factory, - /* Passthorugh only! */ const req_state* s, optional_yield y) const { @@ -560,10 +567,14 @@ rgw::auth::Engine::result_t EC2Engine::authenticate( std::vector admin; } accepted_roles(cct); + /* When we handle a HTTP OPTIONS call we must ignore the signature */ + bool ignore_signature = (s->op_type == RGW_OP_OPTIONS_CORS); + boost::optional t; int failure_reason; std::tie(t, failure_reason) = \ - get_access_token(dpp, access_key_id, string_to_sign, signature, signature_factory); + get_access_token(dpp, access_key_id, string_to_sign, + signature, signature_factory, ignore_signature); if (! t) { if (failure_reason == -ERR_SIGNATURE_NO_MATCH) { // we looked up a secret but it didn't generate the same signature as diff --git a/src/rgw/rgw_auth_keystone.h b/src/rgw/rgw_auth_keystone.h index c9addb0098ace..e68da934e49bf 100644 --- a/src/rgw/rgw_auth_keystone.h +++ b/src/rgw/rgw_auth_keystone.h @@ -151,7 +151,8 @@ class EC2Engine : public rgw::auth::s3::AWSEngine { const std::string_view& access_key_id, const std::string& string_to_sign, const std::string_view& signature, - const signature_factory_t& signature_factory) const; + const signature_factory_t& signature_factory, + bool ignore_signature) const; result_t authenticate(const DoutPrefixProvider* dpp, const std::string_view& access_key_id, const std::string_view& signature, diff --git a/src/rgw/rgw_rest_s3.cc b/src/rgw/rgw_rest_s3.cc index ce5bcd05672f2..5f838e33bb1c0 100644 --- a/src/rgw/rgw_rest_s3.cc +++ b/src/rgw/rgw_rest_s3.cc @@ -5774,6 +5774,13 @@ rgw::auth::s3::LocalEngine::authenticate( } const RGWAccessKey& k = iter->second; + /* Ignore signature for HTTP OPTIONS */ + if (s->op_type == RGW_OP_OPTIONS_CORS) { + auto apl = apl_factory->create_apl_local(cct, s, user_info, + k.subuser, boost::none, access_key_id); + return result_t::grant(std::move(apl), completer_factory(k.key)); + } + const VersionAbstractor::server_signature_t server_signature = \ signature_factory(cct, k.key, string_to_sign); auto compare = signature.compare(server_signature);