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 -<<EOF
+For security reasons, the token file must be readable by the Object Gateway
+only. Also, the Object Gateway should be given a Vault token with a restricted
+policy that allows it to fetch keyrings from a specific path only. Such a policy
+can be created in Vault using the command line utility as in the following
+examples::
+
+ vault policy write rgw-kv-policy -<<EOF
path "secret/data/*" {
capabilities = ["read"]
}
EOF
-A token with the policy above can be created by a Vault administrator as
-follows::
+ vault policy write rgw-transit-policy -<<EOF
+ path "transit/export/encryption-key/*" {
+ capabilities = ["read"]
+ }
+ EOF
+
+Once the policy is created, a token can be generated by a Vault administrator::
- vault token create -policy=rgw-policy
+ vault token create -policy=rgw-kv-policy
Sample output::
token_accessor jv95ZYBUFv6Ss84x7SCSy6lZ
token_duration 768h
token_renewable true
- token_policies ["default" "rgw-policy"]
+ token_policies ["default" "rgw-kv-policy"]
identity_policies []
- policies ["default" "rgw-policy"]
+ policies ["default" "rgw-kv-policy"]
+
+The actual token, displayed in the ``Value`` column of the first line of the
+output, must be saved in a file as plaintext.
+Vault agent
+-----------
-Create a key using KV Engine
-----------------------------
+The Vault agent is a client daemon that provides authentication to Vault and
+manages token renewal and caching. It typically runs on the same host as the
+Object Gateway. With a Vault agent, it is possible to use other Vault
+authentication mechanism such as AppRole, AWS, Certs, JWT, and Azure.
-.. note:: Server-side encryption keys must be 256-bit long and base64 encoded.
+The Object Gateway can be configured to use a Vault agent with the following
+settings::
-To create a key in the KV version 2 engine using Vault's command line tool,
-use the commands below::
+ rgw crypt vault auth = agent
+ rgw crypt vault addr = http://localhost:8100
- export VAULT_ADDR='http://vaultserver:8200'
- vault kv put secret/myproject/mybucketkey key=$(openssl rand -base64 32)
+Vault namespaces
+================
+
+In the Enterprise version, Vault supports the concept of `namespaces`_, which
+allows centralized management for teams within an organization while ensuring
+that those teams operate within isolated environments known as tenants.
+
+The Object Gateway can be configured to access Vault within a particular
+namespace using the following configuration setting::
+
+ rgw crypt vault namespace = tenant1
+
+Create a key in Vault
+=====================
+
+.. note:: Keys for server-side encryption must be 256-bit long and base-64
+ encoded.
-.. important:: 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=<value>.
+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
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=<secret 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
--- -----
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 -<<EOF
- path "transit/export/encryption-key/mybucketkey/*" {
- capabilities = ["read"]
- }
- EOF
+Edit the Ceph configuration file to enable Vault as a KMS backend for
+server-side encryption::
-Once the policy is created, a token can be generated by Vault administrator::
+ rgw crypt s3 kms backend = vault
- vault token create -policy=rgw-transit-policy
+Choose the Vault authentication method, e.g.::
-Sample output::
+ rgw crypt vault auth = token
+ rgw crypt vault token file = /etc/ceph/vault.token
+ rgw crypt vault addr = http://vault-server:8200
- Key Value
- --- -----
- token s.62KuPujbc1234dWB71poOmIZ
- token_accessor jv95ZYBUFv6Ss84x7SCSy6lZ
- token_duration 768h
- token_renewable true
- token_policies ["default" "rgw-transit-policy"]
- identity_policies []
- policies ["default" "rgw-transit-policy"]
+Or::
+ rgw crypt vault auth = agent
+ rgw crypt vault addr = http://localhost:8100
-Configure Ceph Object Gateway
-=================================
+Choose the secrets engine::
+
+ rgw crypt vault secret engine = kv
+
+Or::
+
+ rgw crypt vault secret engine = transit
+
+Optionally, set the Vault namespace where encryption keys will be fetched from::
+
+ rgw crypt vault namespace = tenant1
-Edit the Ceph configuration file to enable Vault as a KMS for server-side
-encryption.::
+Finally, the URLs where the Gateway will retrieve encryption keys from Vault can
+be restricted by setting a path prefix. For instance, the Gateway can be
+restricted to fetch KV keys as follows::
- rgw crypt s3 kms backend = vault
- rgw crypt vault auth = { token | agent }
- rgw crypt vault addr = { vault address e.g. http://localhost:8200 }
- rgw crypt vault prefix = { prefix to secret engine e.g. /v1/transit/export/encryption-key }
- rgw crypt vault token file = { absolute path to token file e.g. /etc/ceph/vault.token }
+ rgw crypt vault prefix = /v1/secret/data
-The following example uses the ``token`` authentication method (with
-a Vault token stored in a file), sets the Vault server address, and restricts
-the URLs where encryption keys can be retrieved from Vault using a path prefix.
-In this example, the Gateway will only fetch transit encryption keys::
+Or, in the case of exportable transit keys::
- rgw crypt s3 kms backend = vault
- rgw crypt vault auth = token
- rgw crypt vault addr = https://vaultserver
- rgw crypt vault secret engine = transit
- rgw crypt vault prefix = /v1/transit/export/encryption-key
- rgw crypt vault token file = /etc/ceph/vault.token
+ rgw crypt vault prefix = /v1/transit/export/encryption-key
+In the example above, the Gateway would only fetch transit encryption keys under
+``http://vault-server:8200/v1/transit/export/encryption-key``.
Upload object
=============
-When uploading an object, provide the SSE key ID in the request. As an example,
-using the AWS command-line client::
+When uploading an object to the Gateway, provide the SSE key ID in the request.
+As an example, using the AWS command-line client::
aws --endpoint=http://radosgw:8000 s3 cp plaintext.txt s3://mybucket/encrypted.txt --sse=aws:kms --sse-kms-key-id myproject/mybucketkey
The Object Gateway will fetch the key from Vault, encrypt the object and store
-it in the bucket. Any request to downlod the object will require the correct key
-ID for the Gateway to successfully decrypt it.
+it in the bucket. Any request to download the object will make the Gateway
+automatically retrieve the correspondent key from Vault and decrypt the object.
Note that the secret will be fetched from Vault using a URL constructed by
concatenating the base address (``rgw crypt vault addr``), the (optional)
URL prefix (``rgw crypt vault prefix``), and finally the key ID. In the example
-above, the Gateway will fetch the secret from::
+above, the Gateway would fetch the secret from::
http://vaultserver:8200/v1/secret/data/myproject/mybucketkey
.. _Server-Side Encryption: ../encryption
.. _Vault: https://www.vaultproject.io/docs/
-.. _token authentication method: https://www.vaultproject.io/docs/auth/token.html
-.. _vault agent: https://www.vaultproject.io/docs/agent/index.html
+.. _Token authentication method: https://www.vaultproject.io/docs/auth/token.html
+.. _Vault agent: https://www.vaultproject.io/docs/agent/index.html
.. _KV Secrets engine: https://www.vaultproject.io/docs/secrets/kv/
.. _Transit engine: https://www.vaultproject.io/docs/secrets/transit
+.. _namespaces: https://www.vaultproject.io/docs/enterprise/namespaces/index.html
@contextlib.contextmanager
def setup_vault(ctx, config):
"""
- Mount simple kv Secret Engine
- Note: this will be extended to support transit secret engine
+ Mount Transit or KV version 2 secrets engine
"""
- data = {
- "type": "kv",
- "options": {
- "version": "2"
+ (cclient, cconfig) = config.items()[0]
+ engine = cconfig.get('engine')
+
+ if engine == 'kv':
+ log.info('Mounting kv version 2 secrets engine')
+ mount_path = '/v1/sys/mounts/kv'
+ data = {
+ "type": "kv",
+ "options": {
+ "version": "2"
+ }
+ }
+ elif engine == 'transit':
+ log.info('Mounting transit secrets engine')
+ mount_path = '/v1/sys/mounts/transit'
+ data = {
+ "type": "transit"
}
- }
+ else:
+ raise Exception("Unknown or missing secrets engine")
- (cclient, cconfig) = config.items()[0]
- log.info('Mount kv version 2 secret engine')
- send_req(ctx, cconfig, cclient, '/v1/sys/mounts/kv', json.dumps(data))
+ send_req(ctx, cconfig, cclient, mount_path, json.dumps(data))
yield
resp = req.getresponse()
log.info(resp.read())
if not (resp.status >= 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:
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
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=
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),
class MockTransitSecretEngine : public TransitSecretEngine {
+
public:
MockTransitSecretEngine(CephContext *cct) : TransitSecretEngine(cct){}
};
+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;
}
};
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);
}
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) {
}
+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:
TEST_F(TestSSEKMS, test_transit_backend_empty_response)
{
-
boost::string_view my_key("/key/nonexistent/1");
std::string actual_key;