From f9d7b685c15e2925e76cb8756937ce1aca91d9ed Mon Sep 17 00:00:00 2001 From: Sergio de Carvalho Date: Thu, 7 Nov 2019 15:33:14 +0000 Subject: [PATCH] rgw: improvements to SSE-KMS with Vault * Minor improvements to Vault documentation * Add teuthology tests for Transit secrets engine * Add unit tests for KV secrets engine, minor improvements to Transit secrets engine * use string_view::npos instead of string::npos Signed-off-by: Andrea Baglioni Signed-off-by: Sergio de Carvalho --- doc/radosgw/vault.rst | 303 +++++++++++------- .../crypt/2-kms/{vault.yaml => vault_kv.yaml} | 2 + qa/suites/rgw/crypt/2-kms/vault_transit.yaml | 23 ++ qa/suites/rgw/crypt/4-tests/s3tests.yaml | 5 +- qa/tasks/s3tests.py | 5 +- qa/tasks/vault.py | 65 ++-- src/rgw/rgw_kms.cc | 6 +- src/test/rgw/test_rgw_kms.cc | 56 +++- src/vstart.sh | 6 +- 9 files changed, 308 insertions(+), 163 deletions(-) rename qa/suites/rgw/crypt/2-kms/{vault.yaml => vault_kv.yaml} (91%) create mode 100644 qa/suites/rgw/crypt/2-kms/vault_transit.yaml diff --git a/doc/radosgw/vault.rst b/doc/radosgw/vault.rst index a384887d3aa..3deef8dac3b 100644 --- a/doc/radosgw/vault.rst +++ b/doc/radosgw/vault.rst @@ -5,75 +5,121 @@ HashiCorp Vault Integration HashiCorp `Vault`_ can be used as a secure key management service for `Server-Side Encryption`_ (SSE-KMS). -#. `Vault Access`_ -#. `Secrets Engines`_ -#. `Configure Ceph Object Gateway`_ +.. ditaa:: +---------+ +---------+ +-------+ +-------+ + | Client | | RadosGW | | Vault | | OSD | + +---------+ +---------+ +-------+ +-------+ + | create secret | | | + | key for key ID | | | + |-----------------+---------------->| | + | | | | + | upload object | | | + | with key ID | | | + |---------------->| request secret | | + | | key for key ID | | + | |---------------->| | + | |<----------------| | + | | return secret | | + | | key | | + | | | | + | | encrypt object | | + | | with secret key | | + | |--------------+ | | + | | | | | + | |<-------------+ | | + | | | | + | | store encrypted | | + | | object | | + | |------------------------------>| + +#. `Vault secrets engines`_ +#. `Vault authentication`_ +#. `Vault namespaces`_ +#. `Create a key in Vault`_ +#. `Configure the Ceph Object Gateway`_ #. `Upload object`_ -Vault Access -============ +Some examples below use the Vault command line utility to interact with +Vault. You may need to set the following environment variable with the correct +address of your Vault server to use this utility:: -Access to Vault can be done through several authentication mechanisms. -Object Gateway supports `token authentication method`_ and `vault agent`_. + export VAULT_ADDR='http://vault-server:8200' -Token Authentication --------------------- -The Token authentication method expects a Vault token to be present in a plaintext file. You can configure -the token option in Ceph's configuration file as follows:: +Vault secrets engines +===================== - rgw crypt vault auth = token - rgw crypt vault token file = /etc/ceph/vault.token +Vault provides several secrets engines, which can store, generate, and encrypt +data. Currently, the Object Gateway supports: -For security reasons, ensure the file is readable by the Object Gateway only. +- `KV secrets engine`_ version 2 +- `Transit engine`_ -.. note:: Token authentication is not recommended configuration for production environments. +KV secrets engine +----------------- -Vault Agent ------------ -Vault Agent is a client daemon that provides authentication to Vault, manages the token renewal and caching. -With a Vault agent, it is possible to use other Vault authentication mechanism such as AppRole, AWS, Certs, JWT, Azure... +The KV secrets engine is used to store arbitrary key/value secrets in Vault. To +enable the KV engine version 2 in Vault, use the following command:: -You can enable agent authentication in Ceph's configuration file with:: + vault secrets enable kv-v2 - rgw crypt vault auth = agent - rgw crypt vault addr = http://localhost:8200 +The Object Gateway can be configured to use the KV engine version 2 with the +following setting:: -.. note:: ``rgw_crypt_vault_token_file`` is ignored if ``rgw_crypt_vault_auth`` is set to ``agent``. + rgw crypt vault secret engine = kv -Secrets Engines -=============== -Vault provides several Secret Engines, which can store, generate, and encrypt data. Currently, the Object Gateway supports: +Transit secrets engine +---------------------- -- `KV Secrets engine`_ version 2 -- `Transit engine`_ +The transit engine handles cryptographic functions on data in-transit. To enable +it in Vault, use the following command:: -.. important:: Examples in this section are for demonstration purposes only and assume token authentication. + vault secrets enable transit +The Object Gateway can be configured to use the transit engine with the +following setting:: -KV Secrets engine ------------------ -To enable the KV engine version 2 in Vault, use the Vault command line -tool:: + rgw crypt vault secret engine = transit - vault secrets enable kv-v2 +Vault authentication +==================== + +Vault supports several authentication mechanisms. Currently, the Object +Gateway can be configured to authenticate to Vault using the +`Token authentication method`_ or a `Vault agent`_. + +Token authentication +-------------------- + +.. note:: Token authentication is not recommended for production environments. -When authenticating using the token method, a token must be obtained -for the Gateway and saved in a file as plain-text. +The token authentication method expects a Vault token to be present in a +plaintext file. The Object Gateway can be configured to use token authentication +with the following settings:: -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 -created in Vault using the Vault command line utility:: + rgw crypt vault auth = token + rgw crypt vault token file = /etc/ceph/vault.token + rgw crypt vault addr = http://vault-server:8200 - vault policy write rgw-policy -<. +Using the KV engine +------------------- -Output:: +A key for server-side encryption can be created in the KV version 2 engine using +the command line utility, as in the following example:: + + vault kv put secret/myproject/mybucketkey key=$(openssl rand -base64 32) + +Sample output:: ====== Metadata ====== Key Value @@ -111,32 +186,26 @@ Output:: destroyed false version 1 - === Data === - Key Value - --- ----- - key Ak5dRyLQjwX/wb7vo6Fq1qjsfk1dh2CiSicX-gLAhwk= - - -Transit Secrets Engine ----------------------- -To enable the Transit engine in Vault, use the Vault command line tool:: +Note that in the KV secrets engine, secrets are stored as key-value pairs, and +the Gateway expects the key name to be ``key``, i.e. the secret must be in the +form ``key=``. - vault secrets enable transit +Using the Transit engine +------------------------ -Create a key using Transit secrets engine ------------------------------------------ -Object Gateway supports Transit engine exportable keys only. -To create an exportable key, use the command line tool:: +Keys created with the Transit engine must be exportable in order to be used for +server-side encryption with the Object Gateway. An exportable key can be created +with the command line utility as follows:: - export VAULT_ADDR='http://vaultserver:8200' - vault write -f transit/keys/mybucketkey exportable=true + vault write -f transit/keys/mybucketkey exportable=true -The command line above creates a keyring, which contains an aes256-gcm96 key type. -To verify the key is created properly, use the command line tool:: +The command above creates a keyring, which contains a key of type +``aes256-gcm96`` by default. To verify that the key was correctly created, use +the following command:: vault read transit/export/encryption-key/mybucketkey/1 -Output:: +Sample output:: Key Value --- ----- @@ -144,82 +213,76 @@ Output:: name mybucketkey type aes256-gcm96 -.. note:: To use Transit Secrets engine in Object Gateway, you shall specify the full key, including its version. +Note that in order to read the key created with the Transit engine, the full +path must be provided including the key version. -For security reasons, the Object Gateway should be given a Vault token with a -restricted policy that allows it to fetch the dedicated keyrings only. Such a policy can be -created in Vault using the Vault command line utility:: +Configure the Ceph Object Gateway +================================= - vault policy write rgw-transit-policy -<= 200 and resp.status < 300): - raise Exception("Error Contacting Vault Server") + raise Exception("Request to Vault server failed with status %d" % resp.status) return resp @contextlib.contextmanager def create_secrets(ctx, config): (cclient, cconfig) = config.items()[0] + engine = cconfig.get('engine') prefix = cconfig.get('prefix') secrets = cconfig.get('secrets') if secrets is None: @@ -166,17 +178,25 @@ def create_secrets(ctx, config): for secret in secrets: try: - data = { - "data": { - "key": secret['secret'] - } - } - except KeyError: - raise ConfigError('vault.secrets must have "secret" field') - try: - send_req(ctx, cconfig, cclient, urljoin(prefix, secret['path']), json.dumps(data)) + path = secret['path'] except KeyError: - raise ConfigError('vault.secrets must have "path" field') + raise ConfigError('Missing "path" field in secret') + + if engine == 'kv': + try: + data = { + "data": { + "key": secret['secret'] + } + } + except KeyError: + raise ConfigError('Missing "secret" field in secret') + elif engine == 'transit': + data = {"exportable": "true"} + else: + raise Exception("Unknown or missing secrets engine") + + send_req(ctx, cconfig, cclient, urljoin(prefix, path), json.dumps(data)) log.info("secrets created") yield @@ -194,6 +214,8 @@ def task(ctx, config): client.0: version: 1.2.2 root_token: test_root_token + engine: kv + prefix: /v1/kv/data/ secrets: - path: kv/teuthology/key_a secret: YmluCmJvb3N0CmJvb3N0LWJ1aWxkCmNlcGguY29uZgo= @@ -220,6 +242,7 @@ def task(ctx, config): ctx.vault.endpoints = assign_ports(ctx, config, 8200) ctx.vault.root_token = None ctx.vault.prefix = config[client].get('prefix') + ctx.vault.engine = config[client].get('engine') with contextutil.nested( lambda: download(ctx=ctx, config=config), diff --git a/src/rgw/rgw_kms.cc b/src/rgw/rgw_kms.cc index bd9a8d94f88..098edeca51b 100644 --- a/src/rgw/rgw_kms.cc +++ b/src/rgw/rgw_kms.cc @@ -93,7 +93,7 @@ protected: ldout(cct, 0) << "Loading Vault Token from filesystem" << dendl; res = load_token_from_file(&vault_token); if (res < 0){ - return res; + return res; } } @@ -177,9 +177,9 @@ private: size_t pos = 0; pos = key_id.rfind("/"); - if (pos != std::string::npos){ + if (pos != boost::string_view::npos){ boost::string_view token = key_id.substr(pos+1, key_id.length()-pos); - if (!token.empty() && token.find_first_not_of("0123456789") == std::string::npos){ + if (!token.empty() && token.find_first_not_of("0123456789") == boost::string_view::npos){ version.assign(std::string(token)); return 0; } diff --git a/src/test/rgw/test_rgw_kms.cc b/src/test/rgw/test_rgw_kms.cc index 494924ec0d2..4246a1c1d8c 100644 --- a/src/test/rgw/test_rgw_kms.cc +++ b/src/test/rgw/test_rgw_kms.cc @@ -14,6 +14,7 @@ using ::testing::MakeAction; class MockTransitSecretEngine : public TransitSecretEngine { + public: MockTransitSecretEngine(CephContext *cct) : TransitSecretEngine(cct){} @@ -21,20 +22,31 @@ public: }; +class MockKvSecretEngine : public KvSecretEngine { + +public: + MockKvSecretEngine(CephContext *cct) : KvSecretEngine(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; + MockKvSecretEngine* kv_engine; void SetUp() override { cct = (new CephContext(CEPH_ENTITY_TYPE_ANY))->get(); transit_engine = new MockTransitSecretEngine(cct); + kv_engine = new MockKvSecretEngine(cct); } void TearDown() { delete transit_engine; + delete kv_engine; } }; @@ -42,25 +54,30 @@ protected: TEST_F(TestSSEKMS, vault_token_file_unset) { + cct->_conf.set_val("rgw_crypt_vault_auth", "token"); + TransitSecretEngine te(cct); + KvSecretEngine kv(cct); 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); + + ASSERT_EQ(te.get_key(key_id, actual_key), -EINVAL); + ASSERT_EQ(kv.get_key(key_id, actual_key), -EINVAL); } TEST_F(TestSSEKMS, non_existent_vault_token_file) { + cct->_conf.set_val("rgw_crypt_vault_auth", "token"); cct->_conf.set_val("rgw_crypt_vault_token_file", "/nonexistent/file"); + TransitSecretEngine te(cct); + KvSecretEngine kv(cct); 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); + ASSERT_EQ(te.get_key(key_id, actual_key), -ENOENT); + ASSERT_EQ(kv.get_key(key_id, actual_key), -ENOENT); } @@ -90,15 +107,13 @@ Action SetPointedValue(std::string json) { 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", ""}; + std::string actual_key; + std::string tests[11] {"/", "my_key/", "my_key", "", "my_key/a", "my_key/1a", + "my_key/a1", "my_key/1a1", "my_key/1/a", "1", "my_key/1/" + }; int res; for (const auto &test: tests) { @@ -128,6 +143,22 @@ TEST_F(TestSSEKMS, test_transit_backend){ } +TEST_F(TestSSEKMS, test_kv_backend){ + + boost::string_view my_key("my_key"); + std::string actual_key; + + // Mocks the expected return value from Vault Server using custom Argument Action + string json = R"({"data": {"data": {"key": "8qgPWvdtf6zrriS5+nkOzDJ14IGVR6Bgkub5dJn6qeg="}}})"; + EXPECT_CALL(*kv_engine, send_request(_, _)).WillOnce(SetPointedValue(json)); + + int res = kv_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: @@ -155,7 +186,6 @@ TEST_F(TestSSEKMS, concat_url) TEST_F(TestSSEKMS, test_transit_backend_empty_response) { - boost::string_view my_key("/key/nonexistent/1"); std::string actual_key; diff --git a/src/vstart.sh b/src/vstart.sh index 848ad245c22..a2a4e35f4c0 100755 --- a/src/vstart.sh +++ b/src/vstart.sh @@ -680,10 +680,12 @@ EOF ; 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/transit/export/encryption-key/ ;rgw crypt vault token file = $CEPH_CONF_PATH/vault.token + ;rgw crypt vault addr = http://127.0.0.1:8200 + ;rgw crypt vault secret engine = kv + ;rgw crypt vault prefix = /v1/kv/data ;rgw crypt vault secret engine = transit + ;rgw crypt vault prefix = /v1/transit/export/encryption-key/ $extra_conf EOF -- 2.39.5