]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: Added caching for tokens retrieved from keystone using S3 credentials
authorJames P. Weaver <james.barrett@bbc.co.uk>
Mon, 14 Jan 2019 16:09:13 +0000 (16:09 +0000)
committerNathan Cutler <ncutler@suse.com>
Tue, 4 May 2021 19:43:21 +0000 (21:43 +0200)
When providing an S3 compatible interface previous behaviour has been to cache the
access token but request verification of the signature for every request from keystone.
This causes keystone to be quite a performance bottleneck especially for instalations
with high levels of S3 traffic.

In this commit a change is made to perform the verification of S3 request signatures
inside the radosgw process, thereby seriously reducing the number of requests that need
to be made to keystone. To do this a shared S3 secret key is obtained from keystone and
kept in a local cache.

Signed-off-by: James Weaver <james.barrett@bbc.co.uk>
(cherry picked from commit f8a82240d0b177451bf958be0a671def098932a6)

src/rgw/rgw_auth_keystone.cc
src/rgw/rgw_auth_keystone.h
src/rgw/rgw_auth_s3.h

index 5a325425ea1b784e5ef67657b5f37bf214cbd810..72c00e0eb66474a2ae5dd38dbf2c427013f6f171 100644 (file)
@@ -21,7 +21,7 @@
 #include "rgw_auth_s3.h"
 
 #include "common/ceph_crypto_cms.h"
-#include "common/armor.h"
+#include "common/ceph_crypto.h"
 #include "common/Cond.h"
 
 #define dout_subsys ceph_subsys_rgw
@@ -380,6 +380,224 @@ EC2Engine::get_from_keystone(const DoutPrefixProvider* dpp, const boost::string_
   return std::make_pair(std::move(token_envelope), 0);
 }
 
+std::string EC2Engine::sign(const DoutPrefixProvider* dpp,
+                           const std::string& secret,
+                           const std::string& string_to_sign) const
+{
+  using ceph::crypto::HMACSHA256;
+
+  /*
+     The extra data needed for the signing is included in the clear in the string_to_sign
+     so we really need to extract it.
+  */
+
+  std::vector<std::string> lines;
+  boost::split(lines, string_to_sign, boost::is_any_of("\r\n"));
+
+  if (lines.size() < 1) {
+    ldpp_dout(dpp, 0) << "String to sign is not in any recognisable format" << dendl;
+    throw -ERR_SIGNATURE_NO_MATCH;
+  }
+
+  if (lines[0] != "AWS4-HMAC-SHA256") {
+    ldpp_dout(dpp, 0) << "Signature format " << lines[0] << "cannot be handled internally" << dendl;
+    throw -ERR_SIGNATURE_NO_MATCH;
+  }
+
+  if (lines.size() < 4) {
+    ldpp_dout(dpp, 0) << "String to sign does not contain all necessary data for AWS4-HMAC-SHA256 signing mode" << dendl;
+    throw -ERR_SIGNATURE_NO_MATCH;
+  }
+  std::string& credential_scope = lines[2];
+
+  std::vector<std::string> scope_data;
+  boost::split(scope_data, credential_scope, boost::is_any_of("/"));
+
+  if (scope_data.size() < 4) {
+    ldpp_dout(dpp, 0) << "String to sign does not contain all necessary credential scope data for AWS4-HMAC-SHA256 signing mode" << dendl;
+    throw -ERR_SIGNATURE_NO_MATCH;
+  }
+
+  std::string& timestamp = scope_data[0];
+  std::string& region_name = scope_data[1];
+  std::string& service_name = scope_data[2];
+  std::string& command_name = scope_data[3];
+
+  if (command_name != "aws4_request") {
+    ldpp_dout(dpp, 0) << "credential_scope is not for aws4_request when using AWS4-HMAC-SHA256 signing mode" << dendl;
+    throw -ERR_SIGNATURE_NO_MATCH;
+  }
+
+  /* With these data we can now construct the signing key */
+  std::string key = std::string("AWS4") + secret;
+
+  unsigned char date_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+  HMACSHA256 date_hash((const unsigned char *)key.c_str(), key.size());
+  date_hash.Update((const unsigned char *)timestamp.c_str(), timestamp.size());
+  date_hash.Final(date_digest);
+
+  unsigned char region_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+  HMACSHA256 region_hash(date_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+  region_hash.Update((const unsigned char *)region_name.c_str(), region_name.size());
+  region_hash.Final(region_digest);
+
+  unsigned char service_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+  HMACSHA256 service_hash(region_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+  service_hash.Update((const unsigned char *)service_name.c_str(), service_name.size());
+  service_hash.Final(service_digest);
+
+  unsigned char command_digest[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+  HMACSHA256 command_hash(service_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+  command_hash.Update((const unsigned char *)"aws4_request", 12);
+  command_hash.Final(command_digest);
+
+  /* And sign the data with it */
+  unsigned char signature[CEPH_CRYPTO_HMACSHA256_DIGESTSIZE];
+  HMACSHA256 hash(command_digest, CEPH_CRYPTO_HMACSHA256_DIGESTSIZE);
+  hash.Update((const unsigned char *)string_to_sign.c_str(), string_to_sign.size());
+  hash.Final(signature);
+
+  std::stringstream ss;
+  for (int i=0; i < CEPH_CRYPTO_HMACSHA256_DIGESTSIZE; i++) {
+    ss << std::setfill('0') << std::setw(2) << std::hex << (int)signature[i];
+  }
+
+  return ss.str();
+}
+
+std::pair<boost::optional<std::string>, int> EC2Engine::get_secret_from_keystone(const DoutPrefixProvider* dpp,
+                                                                                 const std::string& user_id,
+                                                                                 const boost::string_view& access_key_id) const
+{
+  /*  Fetch from /users/{USER_ID}/credentials/OS-EC2/{ACCESS_KEY_ID} */
+  /* Should return json with response key "credential" which contains entry "secret"*/
+
+  /* prepare keystone url */
+  std::string keystone_url = config.get_endpoint_url();
+  if (keystone_url.empty()) {
+    return make_pair(boost::none, -EINVAL);
+  }
+
+  const auto api_version = config.get_api_version();
+  if (config.get_api_version() == rgw::keystone::ApiVersion::VER_3) {
+    keystone_url.append("v3/");
+  } else {
+    keystone_url.append("v2.0/");
+  }
+  keystone_url.append("users/");
+  keystone_url.append(user_id);
+  keystone_url.append("/credentials/OS-EC2/");
+  keystone_url.append(access_key_id.to_string());
+
+  /* get authentication token for Keystone. */
+  std::string admin_token;
+  int ret = rgw::keystone::Service::get_admin_token(cct, token_cache, config,
+                                                    admin_token);
+  if (ret < 0) {
+    ldpp_dout(dpp, 2) << "s3 keystone: cannot get token for keystone access"
+                  << dendl;
+    return make_pair(boost::none, ret);
+  }
+
+  using RGWGetAccessSecret
+    = rgw::keystone::Service::RGWKeystoneHTTPTransceiver;
+
+  /* The container for plain response obtained from Keystone.*/
+  ceph::bufferlist token_body_bl;
+  RGWGetAccessSecret secret(cct, "GET", keystone_url, &token_body_bl);
+
+  /* set required headers for keystone request */
+  secret.append_header("X-Auth-Token", admin_token);
+
+  /* check if we want to verify keystone's ssl certs */
+  secret.set_verify_ssl(cct->_conf->rgw_keystone_verify_ssl);
+
+  /* send request */
+  ret = secret.process();
+  if (ret < 0) {
+    ldpp_dout(dpp, 2) << "s3 keystone: secret fetching error: "
+                  << token_body_bl.c_str() << dendl;
+    return make_pair(boost::none, ret);
+  }
+
+  /* if the supplied signature is wrong, we will get 401 from Keystone */
+  if (secret.get_http_status() ==
+          decltype(secret)::HTTP_STATUS_NOTFOUND) {
+    return make_pair(boost::none, -EINVAL);
+  }
+
+  /* now parse response */
+
+  JSONParser parser;
+  if (! parser.parse(token_body_bl.c_str(), token_body_bl.length())) {
+    ldpp_dout(dpp, 0) << "Keystone credential parse error: malformed json" << dendl;
+    return make_pair(boost::none, -EINVAL);
+  }
+
+  JSONObjIter credential_iter = parser.find_first("credential");
+  std::string secret_string;
+
+  try {
+    if (!credential_iter.end()) {
+      JSONDecoder::decode_json("secret", secret_string, *credential_iter, true);
+    } else {
+      ldpp_dout(dpp, 0) << "Keystone credential not present in return from server" << dendl;
+      return make_pair(boost::none, -EINVAL);
+    }
+  } catch (JSONDecoder::err& err) {
+    ldpp_dout(dpp, 0) << "Keystone credential parse error: " << err.message << dendl;
+    return make_pair(boost::none, -EINVAL);
+  }
+
+  return make_pair(secret_string, 0);
+}
+
+/*
+ * Try to get a token for S3 authentication, using a secret cache if available
+ */
+std::pair<boost::optional<rgw::keystone::TokenEnvelope>, int>
+EC2Engine::get_access_token(const DoutPrefixProvider* dpp,
+                           const boost::string_view& access_key_id,
+                            const std::string& string_to_sign,
+                            const boost::string_view& signature) const
+{
+  boost::optional<rgw::keystone::TokenEnvelope> token;
+  int failure_reason;
+
+  /* Get a token from the cache if one has already been stored */
+  boost::optional<boost::tuple<rgw::keystone::TokenEnvelope, std::string>>
+    t = secret_cache.find(access_key_id.to_string());
+
+  /* Check that credentials can correctly be used to sign data */
+  if (t) {
+    std::string sig(signature);
+    std::string calculated_signature = sign(dpp, t->get<1>(), string_to_sign);
+    if (sig == calculated_signature) {
+      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;
+  }
+
+  /* No cached token, token expired, or secret invalid: fall back to keystone */
+  std::tie(token, failure_reason) = get_from_keystone(dpp, access_key_id, string_to_sign, signature);
+
+  if (token) {
+    /* Fetch secret from keystone for the access_key_id */
+    boost::optional<std::string> secret;
+    std::tie(secret, failure_reason) = get_secret_from_keystone(dpp, token->get_user_id(), access_key_id);
+
+    if (secret) {
+      /* Add token, secret pair to cache, and set timeout */
+      secret_cache.add(access_key_id.to_string(), *token, *secret);
+    }
+  }
+
+  return std::make_pair(token, failure_reason);
+}
+
 EC2Engine::acl_strategy_t
 EC2Engine::get_acl_strategy(const EC2Engine::token_envelope_t&) const
 {
@@ -447,7 +665,7 @@ rgw::auth::Engine::result_t EC2Engine::authenticate(
   boost::optional<token_envelope_t> t;
   int failure_reason;
   std::tie(t, failure_reason) = \
-    get_from_keystone(dpp, access_key_id, string_to_sign, signature);
+    get_access_token(dpp, access_key_id, string_to_sign, signature);
   if (! t) {
     return result_t::deny(failure_reason);
   }
@@ -486,6 +704,63 @@ rgw::auth::Engine::result_t EC2Engine::authenticate(
   }
 }
 
+bool SecretCache::find(const std::string& token_id,
+                       SecretCache::token_envelope_t& token,
+                      std::string &secret)
+{
+  Mutex::Locker l(lock);
+
+  map<std::string, secret_entry>::iterator iter = secrets.find(token_id);
+  if (iter == secrets.end()) {
+    return false;
+  }
+
+  secret_entry& entry = iter->second;
+  secrets_lru.erase(entry.lru_iter);
+
+  const uint64_t now = ceph_clock_now().sec();
+  if (entry.token.expired() || now > entry.expires) {
+    secrets.erase(iter);
+    return false;
+  }
+  token = entry.token;
+  secret = entry.secret;
+
+  secrets_lru.push_front(token_id);
+  entry.lru_iter = secrets_lru.begin();
+
+  return true;
+}
+
+void SecretCache::add(const std::string& token_id,
+                      const SecretCache::token_envelope_t& token,
+                     const std::string& secret)
+{
+  Mutex::Locker l(lock);
+
+  map<string, secret_entry>::iterator iter = secrets.find(token_id);
+  if (iter != secrets.end()) {
+    secret_entry& e = iter->second;
+    secrets_lru.erase(e.lru_iter);
+  }
+
+  const uint64_t now = ceph_clock_now().sec();
+  secrets_lru.push_front(token_id);
+  secret_entry& entry = secrets[token_id];
+  entry.token = token;
+  entry.secret = secret;
+  entry.expires = now + s3_token_expiry_length;
+  entry.lru_iter = secrets_lru.begin();
+
+  while (secrets_lru.size() > max) {
+    list<string>::reverse_iterator riter = secrets_lru.rbegin();
+    iter = secrets.find(*riter);
+    assert(iter != secrets.end());
+    secrets.erase(iter);
+    secrets_lru.pop_back();
+  }
+}
+
 }; /* namespace keystone */
 }; /* namespace auth */
 }; /* namespace rgw */
index e63ba1e3fb3aadad9dcd66297d3f4cb60e020dad..9a3456e91d1c58216f8cb0804d97cf823bc4ecb0 100644 (file)
@@ -72,6 +72,57 @@ public:
   }
 }; /* class TokenEngine */
 
+class SecretCache {
+  using token_envelope_t = rgw::keystone::TokenEnvelope;
+
+  struct secret_entry {
+    token_envelope_t token;
+    std::string secret;
+    uint64_t expires;
+    list<std::string>::iterator lru_iter;
+  };
+
+  const boost::intrusive_ptr<CephContext> cct;
+
+  std::map<std::string, secret_entry> secrets;
+  std::list<std::string> secrets_lru;
+
+  Mutex lock;
+
+  const size_t max;
+
+  const uint64_t s3_token_expiry_length;
+
+  SecretCache()
+    : cct(g_ceph_context),
+      lock("rgw::auth::keystone::SecretCache"),
+      max(cct->_conf->rgw_keystone_token_cache_size),
+      s3_token_expiry_length(300) {
+  }
+
+  ~SecretCache() {}
+
+public:
+  SecretCache(const SecretCache&) = delete;
+  void operator=(const SecretCache&) = delete;
+
+  static SecretCache& get_instance() {
+    /* In C++11 this is thread safe. */
+    static SecretCache instance;
+    return instance;
+  }
+
+  bool find(const std::string& token_id, token_envelope_t& token, std::string& secret);
+  boost::optional<boost::tuple<token_envelope_t, std::string>> find(const std::string& token_id) {
+    token_envelope_t token_envlp;
+    std::string secret;
+    if (find(token_id, token_envlp, secret)) {
+      return boost::make_tuple(token_envlp, secret);
+    }
+    return boost::none;
+  }
+  void add(const std::string& token_id, const token_envelope_t& token, const std::string& secret);
+}; /* class SecretCache */
 
 class EC2Engine : public rgw::auth::s3::AWSEngine {
   using acl_strategy_t = rgw::auth::RemoteApplier::acl_strategy_t;
@@ -82,6 +133,7 @@ class EC2Engine : public rgw::auth::s3::AWSEngine {
   const rgw::auth::RemoteApplier::Factory* const apl_factory;
   rgw::keystone::Config& config;
   rgw::keystone::TokenCache& token_cache;
+  rgw::auth::keystone::SecretCache& secret_cache;
 
   /* Helper methods. */
   acl_strategy_t get_acl_strategy(const token_envelope_t& token) const;
@@ -89,9 +141,15 @@ class EC2Engine : public rgw::auth::s3::AWSEngine {
                              const std::vector<std::string>& admin_roles
                             ) const noexcept;
   std::pair<boost::optional<token_envelope_t>, int>
-  get_from_keystone(const DoutPrefixProvider* dpp, const boost::string_view& access_key_id,
+  get_from_keystone(const DoutPrefixProvider* dpp,
+                    const boost::string_view& access_key_id,
                     const std::string& string_to_sign,
                     const boost::string_view& signature) const;
+  std::pair<boost::optional<token_envelope_t>, int>
+  get_access_token(const DoutPrefixProvider* dpp,
+                   const boost::string_view& access_key_id,
+                   const std::string& string_to_sign,
+                   const boost::string_view& signature) const;
   result_t authenticate(const DoutPrefixProvider* dpp,
                         const boost::string_view& access_key_id,
                         const boost::string_view& signature,
@@ -100,6 +158,10 @@ class EC2Engine : public rgw::auth::s3::AWSEngine {
                         const signature_factory_t&,
                         const completer_factory_t& completer_factory,
                         const req_state* s) const override;
+  std::string sign(const DoutPrefixProvider* dpp, const std::string& secret, const std::string& string_to_sign) const;
+  std::pair<boost::optional<std::string>, int> get_secret_from_keystone(const DoutPrefixProvider* dpp,
+                                                                        const std::string& user_id,
+                                                                        const boost::string_view& access_key_id) const;
 public:
   EC2Engine(CephContext* const cct,
             const rgw::auth::s3::AWSEngine::VersionAbstractor* const ver_abstractor,
@@ -108,11 +170,13 @@ public:
             /* The token cache is used ONLY for the retrieving admin token.
              * Due to the architecture of AWS Auth S3 credentials cannot be
              * cached at all. */
-            rgw::keystone::TokenCache& token_cache)
+            rgw::keystone::TokenCache& token_cache,
+           rgw::auth::keystone::SecretCache& secret_cache)
     : AWSEngine(cct, *ver_abstractor),
       apl_factory(apl_factory),
       config(config),
-      token_cache(token_cache) {
+      token_cache(token_cache),
+      secret_cache(secret_cache) {
   }
 
   using AWSEngine::authenticate;
index 519f839536fc3982baa25729cfa87e8a7a13c036..9f636ebd37ab2e1a829b446bb4dc25d34a823a6b 100644 (file)
@@ -102,6 +102,7 @@ class ExternalAuthStrategy : public rgw::auth::Strategy,
 
   using keystone_config_t = rgw::keystone::CephCtxConfig;
   using keystone_cache_t = rgw::keystone::TokenCache;
+  using secret_cache_t = rgw::auth::keystone::SecretCache;
   using EC2Engine = rgw::auth::keystone::EC2Engine;
 
   boost::optional <EC2Engine> keystone_engine;
@@ -136,7 +137,8 @@ public:
       keystone_engine.emplace(cct, ver_abstractor,
                               static_cast<rgw::auth::RemoteApplier::Factory*>(this),
                               keystone_config_t::get_instance(),
-                              keystone_cache_t::get_instance<keystone_config_t>());
+                              keystone_cache_t::get_instance<keystone_config_t>(),
+                             secret_cache_t::get_instance());
       add_engine(Control::SUFFICIENT, *keystone_engine);
 
     }