]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: Add support to Vault's Transit Secrets Engine in SSE-KMS
authorAndrea Baglioni <andrea.baglioni@workday.com>
Thu, 24 Oct 2019 09:14:54 +0000 (10:14 +0100)
committerAndrea Baglioni <andrea.baglioni@workday.com>
Thu, 28 Nov 2019 09:19:39 +0000 (09:19 +0000)
* refactor rgw_kms.cc to support extension to multiple secret engines.
* introduced support to Vault Namesapces
* added support for Vault Agent

Signed-off-by: Andrea Baglioni <andrea.baglioni@workday.com>
Signed-off-by: Sergio de Carvalho <sergio.carvalho@workday.com>
doc/radosgw/config-ref.rst
doc/radosgw/vault.rst
src/common/legacy_config_opts.h
src/common/options.cc
src/rgw/rgw_kms.cc
src/rgw/rgw_kms.h
src/test/rgw/test_rgw_kms.cc
src/vstart.sh

index c3f17d62824a94c84359f12983e7d670b5df1fa0..5849047be633bb811ef105dd74293be00c49171a 100644 (file)
@@ -55,14 +55,14 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 :Description: The number of entries in the Ceph Object Gateway cache.
 :Type: Integer
 :Default: ``10000``
-       
+
 
 ``rgw socket path``
 
-:Description: The socket path for the domain socket. ``FastCgiExternalServer`` 
-              uses this socket. If you do not specify a socket path, Ceph 
-              Object Gateway will not run as an external server. The path you 
-              specify here must be the same as the path specified in the 
+:Description: The socket path for the domain socket. ``FastCgiExternalServer``
+              uses this socket. If you do not specify a socket path, Ceph
+              Object Gateway will not run as an external server. The path you
+              specify here must be the same as the path specified in the
               ``rgw.conf`` file.
 
 :Type: String
@@ -76,7 +76,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw host``
 
-:Description: The host for the Ceph Object Gateway instance. Can be an IP 
+:Description: The host for the Ceph Object Gateway instance. Can be an IP
               address or a hostname.
 
 :Type: String
@@ -85,9 +85,9 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw port``
 
-:Description: Port the instance listens for requests. If not specified, 
+:Description: Port the instance listens for requests. If not specified,
               Ceph Object Gateway runs external FastCGI.
-              
+
 :Type: String
 :Default: None
 
@@ -95,9 +95,9 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 ``rgw dns name``
 
 :Description: The DNS name of the served domain. See also the ``hostnames`` setting within regions.
-:Type: String 
+:Type: String
 :Default: None
-       
+
 
 ``rgw script uri``
 
@@ -126,8 +126,8 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw remote addr param``
 
-:Description: The remote address parameter. For example, the HTTP field 
-              containing the remote address, or the ``X-Forwarded-For`` 
+:Description: The remote address parameter. For example, the HTTP field
+              containing the remote address, or the ``X-Forwarded-For``
               address if a reverse proxy is operational.
 
 :Type: String
@@ -135,25 +135,25 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 
 ``rgw op thread timeout``
-       
+
 :Description: The timeout in seconds for open threads.
 :Type: Integer
 :Default: 600
-       
+
 
 ``rgw op thread suicide timeout``
-       
-:Description: The time ``timeout`` in seconds before a Ceph Object Gateway 
+
+:Description: The time ``timeout`` in seconds before a Ceph Object Gateway
               process dies. Disabled if set to ``0``.
 
-:Type: Integer 
+:Type: Integer
 :Default: ``0``
 
 
 ``rgw thread pool size``
 
 :Description: The size of the thread pool.
-:Type: Integer 
+:Type: Integer
 :Default: 100 threads.
 
 
@@ -168,7 +168,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw init timeout``
 
-:Description: The number of seconds before Ceph Object Gateway gives up on 
+:Description: The number of seconds before Ceph Object Gateway gives up on
               initialization.
 
 :Type: Integer
@@ -177,7 +177,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw mime types file``
 
-:Description: The path and location of the MIME types. Used for Swift 
+:Description: The path and location of the MIME types. Used for Swift
               auto-detection of object types.
 
 :Type: String
@@ -186,7 +186,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw gc max objs``
 
-:Description: The maximum number of objects that may be handled by 
+:Description: The maximum number of objects that may be handled by
               garbage collection in one garbage collection processing cycle.
 
 :Type: Integer
@@ -195,16 +195,16 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw gc obj min wait``
 
-:Description: The minimum wait time before the object may be removed 
+:Description: The minimum wait time before the object may be removed
               and handled by garbage collection processing.
-              
+
 :Type: Integer
 :Default: ``2 * 3600``
 
 
 ``rgw gc processor max time``
 
-:Description: The maximum time between the beginning of two consecutive garbage 
+:Description: The maximum time between the beginning of two consecutive garbage
               collection processing cycles.
 
 :Type: Integer
@@ -227,7 +227,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw resolve cname``
 
-:Description: Whether ``rgw`` should use DNS CNAME record of the request 
+:Description: Whether ``rgw`` should use DNS CNAME record of the request
               hostname field (if hostname is not equal to ``rgw dns name``).
 
 :Type: Boolean
@@ -258,7 +258,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw exit timeout secs``
 
-:Description: Number of seconds to wait for a process before exiting 
+:Description: Number of seconds to wait for a process before exiting
               unconditionally.
 
 :Type: Integer
@@ -280,7 +280,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 :Type: Integer
 :Default: ``4 << 20``
 
+
 ``rgw relaxed s3 bucket names``
 
 :Description: Enables relaxed S3 bucket names rules for US region buckets.
@@ -312,7 +312,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 
 ``rgw curl wait timeout ms``
 
-:Description: The timeout in milliseconds for certain ``curl`` calls. 
+:Description: The timeout in milliseconds for certain ``curl`` calls.
 :Type: Integer
 :Default: ``1000``
 
@@ -327,7 +327,7 @@ instances or all radosgw-admin commands can be put into the ``[global]`` or the
 ``rgw copy obj progress every bytes``
 
 :Description: The minimum bytes between copy progress output.
-:Type: Integer 
+:Type: Integer
 :Default: ``1024 * 1024``
 
 
@@ -518,8 +518,8 @@ Swift Settings
 :Description: Enforces the Swift Access Control List (ACL) settings.
 :Type: Boolean
 :Default: ``true``
-       
-       
+
+
 ``rgw swift token expiration``
 
 :Description: The time in seconds for expiring a Swift token.
@@ -532,7 +532,7 @@ Swift Settings
 :Description: The URL for the Ceph Object Gateway Swift API.
 :Type: String
 :Default: None
-       
+
 
 ``rgw swift url prefix``
 
@@ -565,7 +565,7 @@ Swift Settings
 
 ``rgw swift auth url``
 
-:Description: Default URL for verifying v1 auth tokens (if not using internal 
+:Description: Default URL for verifying v1 auth tokens (if not using internal
               Swift auth).
 
 :Type: String
@@ -619,7 +619,7 @@ Swift Settings
               Those containers cannot be versioned by the S3 object versioning
               mechanism.
 
-             A slightly different attribute, ``X-History-Location``, which is also understood by 
+             A slightly different attribute, ``X-History-Location``, which is also understood by
               `OpenStack Swift <https://docs.openstack.org/swift/latest/api/object_versioning.html>`_
               for handling ``DELETE`` operations, is currently not supported.
 :Type: Boolean
@@ -644,7 +644,7 @@ Logging Settings
 
 ``rgw log nonexistent bucket``
 
-:Description: Enables Ceph Object Gateway to log a request for a non-existent 
+:Description: Enables Ceph Object Gateway to log a request for a non-existent
               bucket.
 
 :Type: Boolean
@@ -653,7 +653,7 @@ Logging Settings
 
 ``rgw log object name``
 
-:Description: The logging format for an object name. See manpage 
+:Description: The logging format for an object name. See manpage
               :manpage:`date` for details about format specifiers.
 
 :Type: Date
@@ -662,7 +662,7 @@ Logging Settings
 
 ``rgw log object name utc``
 
-:Description: Whether a logged object name includes a UTC time. 
+:Description: Whether a logged object name includes a UTC time.
               If ``false``, it uses the local time.
 
 :Type: Boolean
@@ -678,7 +678,7 @@ Logging Settings
 
 ``rgw usage max user shards``
 
-:Description: The maximum number of shards used for a single user's 
+:Description: The maximum number of shards used for a single user's
               usage logging.
 
 :Type: Integer
@@ -701,7 +701,7 @@ Logging Settings
 
 ``rgw ops log rados``
 
-:Description: Whether the operations log should be written to the 
+:Description: Whether the operations log should be written to the
               Ceph Storage Cluster backend.
 
 :Type: Boolean
@@ -726,7 +726,7 @@ Logging Settings
 
 ``rgw usage log flush threshold``
 
-:Description: The number of dirty merged entries in the usage log before 
+:Description: The number of dirty merged entries in the usage log before
               flushing synchronously.
 
 :Type: Integer
@@ -753,7 +753,7 @@ Logging Settings
 
 ``rgw intent log object name``
 
-:Description: The logging format for the intent log object name. See manpage 
+:Description: The logging format for the intent log object name. See manpage
               :manpage:`date` for details about format specifiers.
 
 :Type: Date
@@ -762,7 +762,7 @@ Logging Settings
 
 ``rgw intent log object name utc``
 
-:Description: Whether the intent log object name includes a UTC time. 
+:Description: Whether the intent log object name includes a UTC time.
               If ``false``, it uses the local time.
 
 :Type: Boolean
@@ -980,6 +980,21 @@ HashiCorp Vault Settings
 :Type: String
 :Default: None
 
+``rgw crypt vault secret engine``
+
+:Description: Vault Secret Engine to be used to retrieve encryption keys: choose
+              between kv-v2, transit.
+:Type: String
+:Default: None
+
+``rgw crypt vault namespace``
+
+:Description: If set, Vault Namespace provides tenant isolation for teams and individuals
+              on the same Vault Enterprise instance, e.g. ``acme/tenant1``
+:Type: String
+:Default: None
+
+
 QoS settings
 ------------
 
index 6bd1a9081f7640631f6ab2dfc110ea70bad49688..d61b3d3a129a8c9ca89996200ae7958311caf8a0 100644 (file)
@@ -14,16 +14,25 @@ Configure Vault
 ===============
 
 Vault provides several Secret Engines, which can store, generate, and encrypt
-data. Currently, the Object Gateway supports the `KV Secrets engine`_ version 2
-only. To enable the KV engine version 2 in Vault, use the Vault command line
+data. Currently, the Object Gateway supports `KV Secrets engine`_ version 2
+an `KV Transit engine`_.
+
+Basic Vault Configuration
+-------------------------
+To enable the KV engine version 2 in Vault, use the Vault command line
 tool::
 
   vault secrets enable kv-v2
 
-Vault also provides several authentication mechanisms. Currently, the Object
-Gateway supports the `token authentication method`_ only. When authenticating
-using the token method, a token must be obtained for the Gateway and saved in a
-file as plain-text.
+Analogously for the Transit Engine::
+  vault secrets enable transit
+
+Vault also provides several authentication mechanisms.
+To simplify user's interaction with Vault, the Object Gateway supports
+two modes: `token authentication method`_ and `agent authentication method`_.
+
+When authenticating using the token method, a token must be obtained
+for the Gateway and saved in a file as plain-text.
 
 For security reasons, the Object Gateway should be given a Vault token with a
 restricted policy that allows it to fetch secrets only. Such a policy can be
@@ -57,6 +66,7 @@ in a file as plain-text. The path to this file must then be provided in the
 Gateway configuration file (see section below). For security reasons, ensure
 the file is readable by the Object Gateway only.
 
+
 Configure the Ceph Object Gateway
 =================================
 
index 6dfeddc8f527d6234abcec379af622000cc99a6b..041cd576656d59ce7b3bdd035a4e3fdf1bb164ae 100644 (file)
@@ -168,7 +168,7 @@ OPTION(ms_async_rdma_type, OPT_STR)
 OPTION(ms_max_accept_failures, OPT_INT)
 
 OPTION(ms_dpdk_port_id, OPT_INT)
-SAFE_OPTION(ms_dpdk_coremask, OPT_STR)        // it is modified in unittest so that use SAFE_OPTION to declare 
+SAFE_OPTION(ms_dpdk_coremask, OPT_STR)        // it is modified in unittest so that use SAFE_OPTION to declare
 OPTION(ms_dpdk_memory_channel, OPT_STR)
 OPTION(ms_dpdk_hugepages, OPT_STR)
 OPTION(ms_dpdk_pmd, OPT_STR)
@@ -545,7 +545,7 @@ OPTION(osd_data, OPT_STR)
 OPTION(osd_journal, OPT_STR)
 OPTION(osd_journal_size, OPT_INT)         // in mb
 OPTION(osd_journal_flush_on_shutdown, OPT_BOOL) // Flush journal to data store on shutdown
-// flags for specific control purpose during osd mount() process. 
+// flags for specific control purpose during osd mount() process.
 // e.g., can be 1 to skip over replaying journal
 // or 2 to skip over mounting omap or 3 to skip over both.
 // This might be helpful in case the journal is totally corrupted
@@ -827,7 +827,7 @@ OPTION(osd_snap_trim_cost, OPT_U32) // set default cost equal to 1MB io
 
 OPTION(osd_scrub_priority, OPT_U32)
 // set default cost equal to 50MB io
-OPTION(osd_scrub_cost, OPT_U32) 
+OPTION(osd_scrub_cost, OPT_U32)
 // set requested scrub priority higher than scrub priority to make the
 // requested scrubs jump the queue of scheduled scrubs
 OPTION(osd_requested_scrub_priority, OPT_U32)
@@ -967,15 +967,15 @@ OPTION(bluestore_compression_max_blob_size_ssd, OPT_U32)
 /*
  * Specifies minimum expected amount of saved allocation units
  * per single blob to enable compressed blobs garbage collection
- * 
+ *
  */
-OPTION(bluestore_gc_enable_blob_threshold, OPT_INT)  
+OPTION(bluestore_gc_enable_blob_threshold, OPT_INT)
 /*
  * Specifies minimum expected amount of saved allocation units
  * per all blobsb to enable compressed blobs garbage collection
- * 
+ *
  */
-OPTION(bluestore_gc_enable_total_threshold, OPT_INT)  
+OPTION(bluestore_gc_enable_total_threshold, OPT_INT)
 
 OPTION(bluestore_max_blob_size, OPT_U32)
 OPTION(bluestore_max_blob_size_hdd, OPT_U32)
@@ -1503,6 +1503,8 @@ OPTION(rgw_crypt_vault_auth, OPT_STR) // Type of authentication method to be use
 OPTION(rgw_crypt_vault_token_file, OPT_STR) // Path to the token file for Vault authentication
 OPTION(rgw_crypt_vault_addr, OPT_STR) // Vault server base address
 OPTION(rgw_crypt_vault_prefix, OPT_STR) // Optional URL prefix to Vault secret path
+OPTION(rgw_crypt_vault_secret_engine, OPT_STR) // kv, transit or other supported secret engines
+OPTION(rgw_crypt_vault_namespace, OPT_STR) // Vault Namespace (only availabe in Vault Enterprise Version)
 
 OPTION(rgw_crypt_s3_kms_encryption_keys, OPT_STR) // extra keys that may be used for aws:kms
                                                       // defined as map "key1=YmluCmJvb3N0CmJvb3N0LQ== key2=b3V0CnNyYwpUZXN0aW5nCg=="
index 67bea89ef3e3618b1c65c76567b04b596e2c8b3d..7ac9ad708dd8ab767b6a382a9b8ef8cca1e0c92f 100644 (file)
@@ -6549,7 +6549,7 @@ std::vector<Option> get_rgw_options() {
     .set_description("Number of seconds the timeout on the reshard locks (bucket reshard lock and reshard log lock) are set to. As a reshard proceeds these locks can be renewed/extended. If too short, reshards cannot complete and will fail, causing a future reshard attempt. If too long a hung or crashed reshard attempt will keep the bucket locked for an extended period, not allowing RGW to detect the failed reshard attempt and recover.")
     .add_tag("performance")
     .add_service("rgw"),
-    
+
     Option("rgw_reshard_batch_size", Option::TYPE_UINT, Option::LEVEL_ADVANCED)
     .set_default(64)
     .set_min(8)
@@ -6597,10 +6597,9 @@ std::vector<Option> get_rgw_options() {
 
     Option("rgw_crypt_vault_auth", Option::TYPE_STR, Option::LEVEL_ADVANCED)
     .set_default("token")
-    .set_enum_allowed({"token"})
+    .set_enum_allowed({"token", "agent"})
     .set_description(
-        "Type of authentication method to be used with Vault. "
-        "The only currently supported method is 'token'.")
+        "Type of authentication method to be used with Vault. ")
     .add_see_also({
         "rgw_crypt_s3_kms_backend",
         "rgw_crypt_vault_addr",
@@ -6633,6 +6632,25 @@ std::vector<Option> get_rgw_options() {
       "rgw_crypt_vault_addr",
       "rgw_crypt_vault_auth"}),
 
+
+    Option("rgw_crypt_vault_secret_engine", Option::TYPE_STR, Option::LEVEL_ADVANCED)
+    .set_enum_allowed({"kv", "transit"})
+    .set_default("transit")
+    .set_description(
+        "Vault Secret Engine to be used to retrieve encryption keys.")
+    .add_see_also({
+      "rgw_crypt_s3_kms_backend",
+      "rgw_crypt_vault_auth",
+      "rgw_crypt_vault_addr"}),
+
+    Option("rgw_crypt_vault_namespace", Option::TYPE_STR, Option::LEVEL_ADVANCED)
+    .set_default("")
+    .set_description("Vault Namespace to be used to select your tenant")
+    .add_see_also({
+      "rgw_crypt_s3_kms_backend",
+      "rgw_crypt_vault_auth",
+      "rgw_crypt_vault_addr"}),
+
     Option("rgw_crypt_suppress_logs", Option::TYPE_BOOL, Option::LEVEL_ADVANCED)
     .set_default(true)
     .set_description("Suppress logs that might print client key"),
index 5a86b25fefcc6f492305c817fb67246463f984e0..22bda18187d12baedc96c28cf5df3f7c37c7f587 100644 (file)
 
 using namespace rgw;
 
-static map<string,string> get_str_map(const string &str) {
-  map<string,string> m;
-  get_str_map(str, &m, ";, \t");
-  return m;
-}
 
 /**
  * Construct a full URL string by concatenating a "base" URL with another path,
@@ -40,6 +35,266 @@ static void concat_url(std::string &url, std::string path) {
   }
 }
 
+
+class VaultSecretEngine: public SecretEngine {
+
+protected:
+  CephContext *cct;
+
+  int load_token_from_file(std::string *vault_token)
+  {
+
+    int res = 0;
+    std::string token_file = cct->_conf->rgw_crypt_vault_token_file;
+    if (token_file.empty()) {
+      ldout(cct, 0) << "ERROR: Vault token file not set in rgw_crypt_vault_token_file" << dendl;
+      return -EINVAL;
+    }
+    ldout(cct, 20) << "Vault token file: " << token_file << dendl;
+
+    struct stat token_st;
+    if (stat(token_file.c_str(), &token_st) != 0) {
+      ldout(cct, 0) << "ERROR: Vault token file '" << token_file << "' not found  " << dendl;
+      return -ENOENT;
+    }
+
+    if (token_st.st_mode & (S_IRWXG | S_IRWXO)) {
+      ldout(cct, 0) << "ERROR: Vault token file '" << token_file << "' permissions are "
+                    << "too open, it must not be accessible by other users" << dendl;
+      return -EACCES;
+    }
+
+    char buf[2048];
+    res = safe_read_file("", token_file.c_str(), buf, sizeof(buf));
+    if (res < 0) {
+      if (-EACCES == res) {
+        ldout(cct, 0) << "ERROR: Permission denied reading Vault token file" << dendl;
+      } else {
+        ldout(cct, 0) << "ERROR: Failed to read Vault token file with error " << res << dendl;
+      }
+      return res;
+    }
+    // drop trailing newlines
+    while (res && isspace(buf[res-1])) {
+      --res;
+    }
+    vault_token->assign(std::string{buf, static_cast<size_t>(res)});
+    memset(buf, 0, sizeof(buf));
+    ::ceph::crypto::zeroize_for_security(buf, sizeof(buf));
+    return res;
+  }
+
+  int send_request(boost::string_view key_id, JSONParser* parser) override
+  {
+    bufferlist secret_bl;
+    int res;
+    string vault_token = "";
+    if (RGW_SSE_KMS_VAULT_AUTH_TOKEN == cct->_conf->rgw_crypt_vault_auth){
+      ldout(cct, 0) << "Loading Vault Token from filesystem" << dendl;
+      res = load_token_from_file(&vault_token);
+      if (res < 0){
+       return res;
+      }
+    }
+
+    std::string secret_url = cct->_conf->rgw_crypt_vault_addr;
+    if (secret_url.empty()) {
+      ldout(cct, 0) << "ERROR: Vault address not set in rgw_crypt_vault_addr" << dendl;
+      return -EINVAL;
+    }
+
+    concat_url(secret_url, cct->_conf->rgw_crypt_vault_prefix);
+    concat_url(secret_url, std::string(key_id));
+
+    RGWHTTPTransceiver secret_req(cct, "GET", secret_url, &secret_bl);
+
+    if (!vault_token.empty()){
+      secret_req.append_header("X-Vault-Token", vault_token);
+      vault_token.replace(0, vault_token.length(), vault_token.length(), '\000');
+    }
+
+    string vault_namespace = cct->_conf->rgw_crypt_vault_namespace;
+    if (!vault_namespace.empty()){
+      ldout(cct, 20) << "Vault Namespace: " << vault_namespace << dendl;
+      secret_req.append_header("X-Vault-Namespace", vault_namespace);
+    }
+
+    res = secret_req.process(null_yield);
+    if (res < 0) {
+      ldout(cct, 0) << "ERROR: Request to Vault failed with error " << res << dendl;
+      return res;
+    }
+
+    if (secret_req.get_http_status() ==
+        RGWHTTPTransceiver::HTTP_STATUS_UNAUTHORIZED) {
+      ldout(cct, 0) << "ERROR: Vault request failed authorization" << dendl;
+      return -EACCES;
+    }
+
+    ldout(cct, 20) << "Request to Vault returned " << res << " and HTTP status "
+      << secret_req.get_http_status() << dendl;
+
+    ldout(cct, 20) << "Parse response into JSON Object" << dendl;
+
+    if (!parser->parse(secret_bl.c_str(), secret_bl.length())) {
+      ldout(cct, 0) << "ERROR: Failed to parse JSON response from Vault" << dendl;
+      return -EINVAL;
+    }
+    secret_bl.zero();
+
+    return res;
+  }
+
+  int decode_secret(JSONObj* json_obj, std::string& actual_key){
+    std::string secret;
+    try {
+      secret = from_base64(json_obj->get_data());
+    } catch (std::exception&) {
+      ldout(cct, 0) << "ERROR: Failed to base64 decode key retrieved from Vault" << dendl;
+      return -EINVAL;
+    }
+
+    actual_key.assign(secret.c_str(), secret.length());
+    secret.replace(0, secret.length(), secret.length(), '\000');
+    return 0;
+  }
+
+public:
+
+  VaultSecretEngine(CephContext *cct) {
+    this->cct = cct;
+  }
+
+  virtual ~VaultSecretEngine(){}
+};
+
+
+class TransitSecretEngine: public VaultSecretEngine {
+
+private:
+  int get_key_version(boost::string_view key_id, string& version)
+  {
+    vector<boost::string_view> tokens;
+    size_t pos = 0;
+
+    pos = key_id.rfind("/", key_id.length());
+    if (pos != std::string::npos){
+      boost::string_view token = key_id.substr(pos, key_id.length());
+      if (tokens.size() >= 1 && token.find_first_not_of("0123456789") == std::string::npos){
+        version.assign(std::string(token));
+        return 0;
+      }
+    }
+    return -1;
+  }
+
+public:
+  TransitSecretEngine(CephContext *cct): VaultSecretEngine(cct){ }
+
+  int get_key(boost::string_view key_id, std::string& actual_key)
+  {
+    JSONParser* parser = new JSONParser();
+    string version;
+
+    if (get_key_version(key_id, version) < 0){
+      ldout(cct, 20) << "Missing or invalid key version" << dendl;
+      return -EINVAL;
+    }
+
+    int res = send_request(key_id, parser);
+    if (res < 0) {
+      return res;
+    }
+
+    JSONObj* json_obj = &(*parser);
+    std::array<std::string, 3> elements = {"data", "keys", version};
+    for(const auto& elem : elements) {
+      json_obj = json_obj->find_obj(elem);
+      if (!json_obj) {
+        ldout(cct, 0) << "ERROR: Key not found in JSON response from Vault using Transit Engine" << dendl;
+        return -EINVAL;
+      }
+    }
+
+    return decode_secret(json_obj, actual_key);
+  }
+
+};
+
+class KvSecretEngine: public VaultSecretEngine {
+
+public:
+
+  KvSecretEngine(CephContext *cct): VaultSecretEngine(cct){ }
+
+  virtual ~KvSecretEngine(){}
+
+  int get_key(boost::string_view key_id, std::string& actual_key){
+    JSONParser parser;
+    int res = send_request(key_id, &parser);
+    if (res < 0) {
+      return res;
+    }
+
+    JSONObj *json_obj = &parser;
+    std::array<std::string, 3> elements = {"data", "data", "key"};
+    for(const auto& elem : elements) {
+      json_obj = json_obj->find_obj(elem);
+      if (!json_obj) {
+        ldout(cct, 0) << "ERROR: Key not found in JSON response from Vault using KV Engine" << dendl;
+        return -EINVAL;
+      }
+    }
+    return decode_secret(json_obj, actual_key);
+  }
+
+};
+
+
+class VaultKMS : KMS {
+
+protected:
+  CephContext *cct;
+  VaultSecretEngine *engine;
+
+public:
+  int get_key(boost::string_view key_id, std::string& actual_key) {
+    if (!engine){
+      ldout(cct, 5) << "ERROR: No supported Vault Secret Engine found" << dendl;
+      return -EINVAL;
+    }
+    return engine->get_key(key_id, actual_key);
+  }
+
+  VaultKMS(CephContext *cct) {
+    std::string secret_engine = cct->_conf->rgw_crypt_vault_secret_engine;
+
+    ldout(cct, 10) << "Selected " << secret_engine << " secret engine" << dendl;
+
+    if (RGW_SSE_KMS_VAULT_SE_KV == secret_engine)
+      engine = new KvSecretEngine(cct);
+    else if (RGW_SSE_KMS_VAULT_SE_TRANSIT == secret_engine)
+      engine = new TransitSecretEngine(cct);
+    else
+      ldout(cct, 0) << "Missing or invalid secret engine" << dendl;
+
+    this->cct = cct;
+  }
+
+  ~VaultKMS(){
+    delete engine;
+  }
+
+};
+
+
+static map<string,string> get_str_map(const string &str) {
+  map<string,string> m;
+  get_str_map(str, &m, ";, \t");
+  return m;
+}
+
+
 static int get_actual_key_from_conf(CephContext *cct,
                                     boost::string_view key_id,
                                     boost::string_view key_selector,
@@ -141,128 +396,16 @@ static int get_actual_key_from_barbican(CephContext *cct,
   return res;
 }
 
-static int request_key_from_vault_with_token(CephContext *cct,
-                                             boost::string_view key_id,
-                                             bufferlist *secret_bl)
-{
-  std::string token_file, secret_url, vault_token;
-  int res = 0;
-
-  token_file = cct->_conf->rgw_crypt_vault_token_file;
-  if (token_file.empty()) {
-    ldout(cct, 0) << "ERROR: Vault token file not set in rgw_crypt_vault_token_file" << dendl;
-    return -EINVAL;
-  }
-  ldout(cct, 20) << "Vault token file: " << token_file << dendl;
-
-  struct stat token_st;
-  if (stat(token_file.c_str(), &token_st) != 0) {
-    ldout(cct, 0) << "ERROR: Vault token file '" << token_file << "' not found  " << dendl;
-    return -ENOENT;
-  }
-
-  if (token_st.st_mode & (S_IRWXG | S_IRWXO)) {
-    ldout(cct, 0) << "ERROR: Vault token file '" << token_file << "' permissions are "
-                  << "too open, it must not be accessible by other users" << dendl;
-    return -EACCES;
-  }
-
-  char buf[2048];
-  res = safe_read_file("", token_file.c_str(), buf, sizeof(buf));
-  if (res < 0) {
-    if (-EACCES == res) {
-      ldout(cct, 0) << "ERROR: Permission denied reading Vault token file" << dendl;
-    } else {
-      ldout(cct, 0) << "ERROR: Failed to read Vault token file with error " << res << dendl;
-    }
-    return res;
-  }
-  // drop trailing newlines
-  while (res && isspace(buf[res-1])) {
-    --res;
-  }
-  vault_token = std::string{buf, static_cast<size_t>(res)};
-  ::ceph::crypto::zeroize_for_security(buf, sizeof(buf));
-
-  secret_url = cct->_conf->rgw_crypt_vault_addr;
-  if (secret_url.empty()) {
-    ldout(cct, 0) << "ERROR: Vault address not set in rgw_crypt_vault_addr" << dendl;
-    return -EINVAL;
-  }
-  concat_url(secret_url, cct->_conf->rgw_crypt_vault_prefix);
-  concat_url(secret_url, std::string(key_id));
-
-  RGWHTTPTransceiver secret_req(cct, "GET", secret_url, secret_bl);
-  secret_req.append_header("X-Vault-Token", vault_token);
-  vault_token.replace(0, vault_token.length(), vault_token.length(), '\000');
-  res = secret_req.process(null_yield);
-  if (res < 0) {
-    ldout(cct, 0) << "ERROR: Request to Vault failed with error " << res << dendl;
-    return res;
-  }
-
-  if (secret_req.get_http_status() ==
-      RGWHTTPTransceiver::HTTP_STATUS_UNAUTHORIZED) {
-    ldout(cct, 0) << "ERROR: Vault request failed authorization" << dendl;
-    return -EACCES;
-  }
-
-  ldout(cct, 20) << "Request to Vault returned " << res << " and HTTP status "
-    << secret_req.get_http_status() << dendl;
-  return res;
-}
 
 static int get_actual_key_from_vault(CephContext *cct,
                                      boost::string_view key_id,
                                      std::string& actual_key)
 {
-  int res = 0;
-  std::string auth;
-  bufferlist secret_bl;
-
-  auth = cct->_conf->rgw_crypt_vault_auth;
-  ldout(cct, 20) << "Vault authentication method: " << auth << dendl;
-
-  // Currently only token-based authentication is supported
-  if (RGW_SSE_KMS_VAULT_AUTH_TOKEN == auth) {
-    res = request_key_from_vault_with_token(cct, key_id, &secret_bl);
-  } else {
-    ldout(cct, 0) << "ERROR: Invalid rgw_crypt_vault_auth: " << auth << dendl;
-    return -EINVAL;
-  }
-
-  if (res < 0) {
-    return res;
-  }
-
-  JSONParser parser;
-  if (!parser.parse(secret_bl.c_str(), secret_bl.length())) {
-    ldout(cct, 0) << "ERROR: Failed to parse JSON response from Vault" << dendl;
-    return -EINVAL;
-  }
-  secret_bl.zero();
-
-  JSONObj *json_obj = &parser;
-  std::array<std::string, 3> elements = {"data", "data", "key"};
-  for(const auto& elem : elements) {
-    json_obj = json_obj->find_obj(elem);
-    if (!json_obj) {
-      ldout(cct, 0) << "ERROR: Key not found in JSON response from Vault" << dendl;
-      return -EINVAL;
-    }
-  }
-
-  std::string secret;
-  try {
-    secret = from_base64(json_obj->get_data());
-  } catch (std::exception&) {
-    ldout(cct, 0) << "ERROR: Failed to base64 decode key retrieved from Vault" << dendl;
-    return -EINVAL;
-  }
-
-  actual_key.assign(secret.c_str(), secret.length());
-  secret.replace(0, secret.length(), secret.length(), '\000');
-
+  ldout(cct, 20) << "Vault authentication method: " << cct->_conf->rgw_crypt_vault_auth << dendl;
+  ldout(cct, 20) << "Vault Secrets Engine: " << cct->_conf->rgw_crypt_vault_secret_engine << dendl;
+  VaultKMS *kms = new VaultKMS(cct);
+  int res = kms->get_key(key_id, actual_key);
+  delete kms;
   return res;
 }
 
index 3e8410e084f0abf96d674796bac2b680c119ceb6..b75ec91f82c1e2aa40d0ef3bff3452a8eca0b8cd 100644 (file)
@@ -15,6 +15,9 @@ static const std::string RGW_SSE_KMS_BACKEND_VAULT = "vault";
 static const std::string RGW_SSE_KMS_VAULT_AUTH_TOKEN = "token";
 static const std::string RGW_SSE_KMS_VAULT_AUTH_AGENT = "agent";
 
+static const std::string RGW_SSE_KMS_VAULT_SE_TRANSIT = "transit";
+static const std::string RGW_SSE_KMS_VAULT_SE_KV = "kv";
+
 /**
  * Retrieves the actual server-side encryption key from a KMS system given a
  * key ID. Currently supported KMS systems are OpenStack Barbican and HashiCorp
@@ -30,4 +33,27 @@ int get_actual_key_from_kms(CephContext *cct,
                             boost::string_view key_selector,
                             std::string& actual_key);
 
+/**
+ * SecretEngine Interface
+ * Defining interface here such that we can use both a real implementation
+ * of this interface, and a mock implementation in tests.
+**/
+class SecretEngine {
+
+public:
+  virtual int get_key(boost::string_view key_id, std::string& actual_key) = 0;
+  virtual ~SecretEngine(){};
+protected:
+  virtual int send_request(boost::string_view key_id, JSONParser* parser) = 0;
+  virtual int decode_secret(JSONObj* json_obj, std::string& actual_key) = 0;
+};
+
+class KMS {
+
+public:
+  virtual int get_key(boost::string_view key_id, std::string& actual_key) = 0;
+  virtual ~KMS(){};
+};
+
+
 #endif
index d1f4584358121dd99a64b9adffdd84ccf1970d94..494924ec0d2bb414e5aac1d15529c4547d3e6507 100644 (file)
 // vim: ts=8 sw=2 smarttab
 
 #include <gtest/gtest.h>
+#include <gmock/gmock.h>
 #include "common/ceph_context.h"
 #include "rgw/rgw_common.h"
 #include "rgw/rgw_kms.cc"
 
-TEST(TestSSEKMS, vault_token_file_unset)
+using ::testing::_;
+using ::testing::Action;
+using ::testing::ActionInterface;
+using ::testing::MakeAction;
+
+
+class MockTransitSecretEngine : public TransitSecretEngine {
+public:
+  MockTransitSecretEngine(CephContext *cct) : TransitSecretEngine(cct){}
+
+  MOCK_METHOD(int, send_request, (boost::string_view key_id, JSONParser* parser), (override));
+
+};
+
+
+class TestSSEKMS : public ::testing::Test {
+
+protected:
+  CephContext *cct;
+  MockTransitSecretEngine* transit_engine;
+
+  void SetUp() override {
+    cct = (new CephContext(CEPH_ENTITY_TYPE_ANY))->get();
+    transit_engine = new MockTransitSecretEngine(cct);
+  }
+
+  void TearDown() {
+    delete transit_engine;
+  }
+
+};
+
+
+TEST_F(TestSSEKMS, vault_token_file_unset)
 {
-  CephContext *cct = (new CephContext(CEPH_ENTITY_TYPE_ANY))->get();
-
-  std::string key_id, actual_key;
-  bufferlist secret_bl;
-  ASSERT_EQ(
-      request_key_from_vault_with_token(cct, key_id, &secret_bl),
-      -EINVAL
-  );
+
+  boost::string_view key_id("my_key");
+  std::string actual_key;
+  TransitSecretEngine te(cct);
+  int res = te.get_key(key_id, actual_key);
+  ASSERT_EQ(res, -EINVAL);
 }
 
-TEST(TestSSEKMS, non_existent_vault_token_file)
+
+TEST_F(TestSSEKMS, non_existent_vault_token_file)
 {
-  CephContext *cct = (new CephContext(CEPH_ENTITY_TYPE_ANY))->get();
   cct->_conf.set_val("rgw_crypt_vault_token_file", "/nonexistent/file");
 
-  std::string key_id, key_selector, actual_key;
-  bufferlist secret_bl;
-  ASSERT_EQ(
-      request_key_from_vault_with_token(cct, key_id, &secret_bl),
-      -ENOENT
-  );
+  boost::string_view key_id("my_key/1");
+  std::string actual_key;
+  TransitSecretEngine te(cct);
+  int res = te.get_key(key_id, actual_key);
+
+  ASSERT_EQ(res, -ENOENT);
+}
+
+
+typedef int SendRequestMethod(boost::string_view, JSONParser*);
+
+class SetPointedValueAction : public ActionInterface<SendRequestMethod> {
+ public:
+  string json;
+
+  SetPointedValueAction(std::string json){
+    this->json = json;
+  }
+
+  int Perform(const ::std::tuple<boost::string_view, JSONParser*>& args) override {
+    JSONParser* p = ::std::get<1>(args);
+    JSONParser* parser = new JSONParser();
+
+    parser->parse(json.c_str(), json.length());
+    *p = *parser;
+    return 0;
+  }
+};
+
+Action<SendRequestMethod> SetPointedValue(std::string json) {
+  return MakeAction(new SetPointedValueAction(json));
 }
 
-TEST(TestSSEKMS, concat_url)
+
+TEST_F(TestSSEKMS, test_transit_key_version_extraction){
+
+  boost::string_view key_id("my_key");
+  std::string version;
+  std::string actual_key;
+
+  string json = R"({"data": {"keys": {"6": "8qgPWvdtf6zrriS5+nkOzDJ14IGVR6Bgkub5dJn6qeg="}}})";
+  EXPECT_CALL(*transit_engine, send_request(_, _)).WillOnce(SetPointedValue(json));
+
+  std::string tests[4] {"/", "my_key/", "my_key", ""};
+
+  int res;
+  for (const auto &test: tests) {
+    res = transit_engine->get_key(boost::string_view(test), actual_key);
+    ASSERT_EQ(res, -EINVAL);
+  }
+
+  res = transit_engine->get_key(boost::string_view("1/2/3/4/5/6"), actual_key);
+  ASSERT_EQ(res, 0);
+  ASSERT_EQ(actual_key, from_base64("8qgPWvdtf6zrriS5+nkOzDJ14IGVR6Bgkub5dJn6qeg="));
+}
+
+
+TEST_F(TestSSEKMS, test_transit_backend){
+
+  boost::string_view my_key("my_key/1");
+  std::string actual_key;
+
+  // Mocks the expected return Value from Vault Server using custom Argument Action
+  string json = R"({"data": {"keys": {"1": "8qgPWvdtf6zrriS5+nkOzDJ14IGVR6Bgkub5dJn6qeg="}}})";
+  EXPECT_CALL(*transit_engine, send_request(_, _)).WillOnce(SetPointedValue(json));
+
+  int res = transit_engine->get_key(my_key, actual_key);
+
+  ASSERT_EQ(res, 0);
+  ASSERT_EQ(actual_key, from_base64("8qgPWvdtf6zrriS5+nkOzDJ14IGVR6Bgkub5dJn6qeg="));
+}
+
+
+TEST_F(TestSSEKMS, concat_url)
 {
   // Each test has 3 strings:
   // * the base URL
@@ -53,4 +150,22 @@ TEST(TestSSEKMS, concat_url)
     concat_url(url, path);
     ASSERT_EQ(url, expected);
   }
-}
\ No newline at end of file
+}
+
+
+TEST_F(TestSSEKMS, test_transit_backend_empty_response)
+{
+
+  boost::string_view my_key("/key/nonexistent/1");
+  std::string actual_key;
+
+  // Mocks the expected return Value from Vault Server using custom Argument Action
+  string json = R"({"errors": ["version does not exist or cannot be found"]})";
+  EXPECT_CALL(*transit_engine, send_request(_, _)).WillOnce(SetPointedValue(json));
+
+  int res = transit_engine->get_key(my_key, actual_key);
+
+  ASSERT_EQ(res, -EINVAL);
+  ASSERT_EQ(actual_key, from_base64(""));
+}
+
index abc3f676f9c2dec26d6938d4612454c2b946d410..848ad245c22c8e2da1f5c54ee4a2cc567121e86a 100755 (executable)
@@ -68,7 +68,7 @@ if [ -e CMakeCache.txt ]; then
     fi
 fi
 
-# use CEPH_BUILD_ROOT to vstart from a 'make install' 
+# use CEPH_BUILD_ROOT to vstart from a 'make install'
 if [ -n "$CEPH_BUILD_ROOT" ]; then
     [ -z "$CEPH_BIN" ] && CEPH_BIN=$CEPH_BUILD_ROOT/bin
     [ -z "$CEPH_LIB" ] && CEPH_LIB=$CEPH_BUILD_ROOT/lib
@@ -100,7 +100,7 @@ export PYTHONPATH=$PYBIND:$CYTHON_PYTHONPATH:$CEPH_PYTHON_COMMON$PYTHONPATH
 export LD_LIBRARY_PATH=$CEPH_LIB:$LD_LIBRARY_PATH
 export DYLD_LIBRARY_PATH=$CEPH_LIB:$DYLD_LIBRARY_PATH
 # Suppress logging for regular use that indicated that we are using a
-# development version. vstart.sh is only used during testing and 
+# development version. vstart.sh is only used during testing and
 # development
 export CEPH_DEV=1
 
@@ -678,11 +678,12 @@ EOF
         ; needed for passing lc time based s3-tests (can be verbose)
         ; rgw lc debug interval = 10
         ; The following settings are for SSE-KMS with Vault
-        ; rgw crypt s3 kms backend = vault
-        ; rgw crypt vault auth = token
-        ; rgw crypt vault addr = http://127.0.0.1:8200
-        ; rgw crypt vault prefix = /v1/secret/data
-        ; rgw crypt vault token file = $CEPH_CONF_PATH/vault.token
+        ;rgw crypt s3 kms backend = vault
+        ;rgw crypt vault auth = token
+        ;rgw crypt vault addr = http://127.0.0.1:8200
+        ;rgw crypt vault prefix = /v1/transit/export/encryption-key/
+        ;rgw crypt vault token file = $CEPH_CONF_PATH/vault.token
+        ;rgw crypt vault secret engine = transit
 
 $extra_conf
 EOF