See Barbican documentation for `How to Create a Secret`_. Requests to
Barbican must include a valid Keystone token in the ``X-Auth-Token`` header.
+.. note:: Server-side encryption keys must be 256-bit long and base64 encoded.
+
Example request::
POST /v1/secrets HTTP/1.1
HashiCorp Vault Settings
========================
-``rgw crypt vault auth```
+``rgw crypt vault auth``
:Description: Type of authentication method to be used. The only method
currently supported is ``token``.
``rgw crypt vault addr``
-:Description: Base URL to the Vault server.
+:Description: Vault server base address, e.g. ``http://vaultserver:8200``.
+:Type: String
+:Default: None
+
+``rgw crypt vault prefix``
+
+:Description: The Vault secret URL prefix, which can be used to restrict access
+ to a particular subset of the secret space, e.g. ``/v1/secret/data``.
:Type: String
:Default: None
for SSL termination, ``rgw trust forwarded https`` must be enabled
before forwarded requests will be trusted as secure.
+.. note:: Server-side encryption keys must be 256-bit long and base64 encoded.
+
Customer-Provided Keys
======================
HashiCorp `Vault`_ can be used as a secure key management service for
`Server-Side Encryption`_ (SSE-KMS).
-#. `Vault authentication`_
-#. `Create a key in Vault`_
+#. `Configure Vault`_
#. `Configure the Ceph Object Gateway`_
+#. `Create a key in Vault`_
#. `Upload object`_
-Vault authentication
-====================
+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
+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.
+
+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::
+
+ vault policy write rgw-policy -<<EOF
+ path "secret/data/*" {
+ capabilities = ["read"]
+ }
+ EOF
+
+A token with the policy above can be created by a Vault administrator as
+follows::
+
+ vault token create -policy=rgw-policy
+
+Sample output::
+
+ Key Value
+ --- -----
+ token s.72KuPujbc065OdWB71poOmIq
+ token_accessor jv95ZYBUFv6Ss84x7SCSy6lZ
+ token_duration 768h
+ token_renewable true
+ token_policies ["default" "rgw-policy"]
+ identity_policies []
+ policies ["default" "rgw-policy"]
+
+The actual token, displayed in the output of the above command, must be saved
+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
+=================================
+
+Edit the Ceph configuration file to enable Vault as a KMS for server-side
+encryption. 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::
-Vault provides several authentication mechanisms. Currently, the Object Gateway
-supports the `token authentication method`_ only.
+ rgw crypt s3 kms backend = vault
+ rgw crypt vault auth = token
+ rgw crypt vault addr = http://vaultserver:8200
+ rgw crypt vault prefix = /v1/secret/data
+ rgw crypt vault token file = /etc/ceph/vault.token
-When authenticating with Vault using the token method, save the token in a
-plain-text file. The path to this file must be provided in the Gateway
-configuration file (see below). For security reasons, ensure the file is
-readable by the Object Gateway only.
+In this example, the Gateway will only fetch encryption keys under
+``http://vaultserver:8200/v1/secret/data``.
Create a key in Vault
=====================
-Generate and save a 256-bit key in Vault. Vault provides several Secret
-Engines, which store, generate, and encrypt data. Currently, the only secret
-engine supported is the `KV Secrets engine`_ version 2.
+.. note:: Server-side encryption keys must be 256-bit long and base64 encoded.
-To create a key in the KV version 2 engine using Vault's command line client,
+To create a key in the KV version 2 engine using Vault's command line tool,
use the commands below::
export VAULT_ADDR='http://vaultserver:8200'
- vault kv put secret/myproject/mybucketkey key=$(dd bs=32 count=1 if=/dev/urandom of=/dev/stdout 2>/dev/null | base64)
+ vault kv put secret/myproject/mybucketkey key=$(openssl rand -base64 32)
Output::
--- -----
key Ak5dRyLQjwX/wb7vo6Fq1qjsfk1dh2CiSicX+gLAhwk=
-The URL to the secret in Vault must be provided in the Gateway configuration
-file (see below).
-
-Configure the Ceph Object Gateway
-=================================
-
-Edit the Ceph configuration file to enable Vault as a KMS for server-side
-encryption::
-
- rgw crypt s3 kms backend = vault
- rgw crypt vault auth = token
- rgw crypt vault addr = http://vaultserver:8200
- rgw crypt vault token file = /path/to/token.file
-
Upload object
=============
When uploading an object, 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 /v1/secret/data/myproject/mybucketkey
+ 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.
+
+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::
-The object gateway will fetch the key from Vault (using the token for
-authentication), 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 the decrypt it.
+ http://vaultserver:8200/v1/secret/data/myproject/mybucketkey
.. _Server-Side Encryption: ../encryption
.. _Vault: https://www.vaultproject.io/docs/
client:
rgw crypt s3 kms backend: vault
rgw crypt vault auth: token
+ rgw crypt vault prefix: /v1/kv/data
rgw:
client.0:
use-vault-role: client.0
tasks:
- vault:
client.0:
- version: 1.2.2
+ install_url: https://releases.hashicorp.com/vault/1.2.2/vault_1.2.2_linux_amd64.zip
+ install_sha256: 7725b35d9ca8be3668abe63481f0731ca4730509419b4eb29fa0b0baa4798458
root_token: test_root_token
+ prefix: /v1/kv/data/
secrets:
- - path: /v1/kv/data/my-key-1
+ - path: my-key-1
secret: a2V5MS5GcWVxKzhzTGNLaGtzQkg5NGVpb1FKcFpGb2c=
- - path: /v1/kv/data/my-key-2
+ - path: my-key-2
secret: a2V5Mi5yNUNNMGFzMVdIUVZxcCt5NGVmVGlQQ1k4YWg=
kms_key: my-key-1
kms_key2: my-key-2
vault:
- key_path: /v1/kv/data/my-key-1
- key_path2: /v1/kv/data/my-key-2
+ key_path: my-key-1
+ key_path2: my-key-2
conf:
client:
rgw lc debug interval: 10
+ rgw crypt s3 kms backend: testing
rgw crypt s3 kms encryption keys: testkey-1=YmluCmJvb3N0CmJvb3N0LWJ1aWxkCmNlcGguY29uZgo= testkey-2=aWIKTWFrZWZpbGUKbWFuCm91dApzcmMKVGVzdGluZwo=
rgw crypt require ssl: false
conf:
client:
rgw lc debug interval: 10
+ rgw crypt s3 kms backend: testing
rgw crypt s3 kms encryption keys: testkey-1=YmluCmJvb3N0CmJvb3N0LWJ1aWxkCmNlcGguY29uZgo= testkey-2=aWIKTWFrZWZpbGUKbWFuCm91dApzcmMKVGVzdGluZwo=
rgw crypt require ssl: false
raise ConfigError('vault: no "root_token" specified')
# create token on file
ctx.cluster.only(client).run(args=['echo', '-n', ctx.vault.root_token, run.Raw('>'), token_path])
+ log.info("Restrict access to token file")
+ ctx.cluster.only(client).run(args=['chmod', '600', token_path])
log.info("Token file content")
ctx.cluster.only(client).run(args=['cat', token_path])
import contextlib
import logging
import time
-
import httplib
import json
+from os import path
+from urlparse import urljoin
from teuthology import misc as teuthology
from teuthology import contextutil
testdir = teuthology.get_testdir(ctx)
for (client, cconf) in config.items():
- vault_version = cconf.get('version', '1.2.2')
-
+ install_url = cconf.get('install_url')
+ install_sha256 = cconf.get('install_sha256')
+ if not install_url or not install_sha256:
+ raise ConfigError("Missing Vault install_url and/or install_sha256")
+ install_zip = path.join(testdir, 'vault.zip')
+ install_dir = path.join(testdir, 'vault')
+
+ log.info('Downloading Vault...')
ctx.cluster.only(client).run(
- args=['mkdir', '-p', '{tdir}/vault'.format(tdir=testdir)])
+ args=['curl', '-L', install_url, '-o', install_zip])
- cmd = [
- 'curl', '-L',
- 'https://releases.hashicorp.com/vault/{version}/vault_{version}_linux_amd64.zip'.format(version=vault_version), '-o',
- '{tdir}/vault_{version}.zip'.format(tdir=testdir, version=vault_version)
- ]
- ctx.cluster.only(client).run(args=cmd)
+ log.info('Verifying SHA256 signature...')
+ ctx.cluster.only(client).run(
+ args=['echo', ' '.join([install_sha256, install_zip]), run.Raw('|'),
+ 'sha256sum', '--check', '--status'])
log.info('Extracting vault...')
+ ctx.cluster.only(client).run(args=['mkdir', '-p', install_dir])
# Using python in case unzip is not installed on hosts
- cmd = ['python', '-m', 'zipfile', '-e',
- '{tdir}/vault_{version}.zip'.format(tdir=testdir, version=vault_version),
- '{tdir}/vault'.format(tdir=testdir)]
- ctx.cluster.only(client).run(args=cmd)
+ ctx.cluster.only(client).run(
+ args=['python', '-m', 'zipfile', '-e', install_zip, install_dir])
try:
yield
testdir = teuthology.get_testdir(ctx)
for client in config:
ctx.cluster.only(client).run(
- args=[
- 'rm',
- '-rf',
- '{tdir}/vault_{version}.zip'.format(tdir=testdir, version=vault_version),
- '{tdir}/vault'.format(tdir=testdir),
- ],
- )
+ args=['rm', '-rf', install_dir, install_zip])
def get_vault_dir(ctx):
host, port = ctx.vault.endpoints[client]
req = httplib.HTTPConnection(host, port, timeout=30)
token = cconfig.get('root_token', 'atoken')
- log.info("Send request to Vault: %s:%s with token: %s", host, port, token)
+ log.info("Send request to Vault: %s:%s at %s with token: %s", host, port, path, token)
headers = {'X-Vault-Token': token}
req.request(method, path, headers=headers, body=body)
resp = req.getresponse()
@contextlib.contextmanager
def create_secrets(ctx, config):
(cclient, cconfig) = config.items()[0]
+ prefix = cconfig.get('prefix')
secrets = cconfig.get('secrets')
if secrets is None:
raise ConfigError("No secrets specified, please specify some.")
except KeyError:
raise ConfigError('vault.secrets must have "secret" field')
try:
- send_req(ctx, cconfig, cclient, secret['path'], json.dumps(data))
+ send_req(ctx, cconfig, cclient, urljoin(prefix, secret['path']), json.dumps(data))
except KeyError:
raise ConfigError('vault.secrets must have "path" field')
ctx.vault = argparse.Namespace()
ctx.vault.endpoints = assign_ports(ctx, config, 8200)
ctx.vault.root_token = None
+ ctx.vault.prefix = config[client].get('prefix')
with contextutil.nested(
lambda: download(ctx=ctx, config=config),
OPTION(rgw_crypt_s3_kms_backend, OPT_STR) // Where SSE-KMS encryption keys are stored
OPTION(rgw_crypt_vault_auth, OPT_STR) // Type of authentication method to be used with Vault
OPTION(rgw_crypt_vault_token_file, OPT_STR) // Path to the token file for Vault authentication
-OPTION(rgw_crypt_vault_addr, OPT_STR) // URL to Vault server endpoint
+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_s3_kms_encryption_keys, OPT_STR) // extra keys that may be used for aws:kms
// defined as map "key1=YmluCmJvb3N0CmJvb3N0LQ== key2=b3V0CnNyYwpUZXN0aW5nCg=="
Option("rgw_crypt_vault_addr", Option::TYPE_STR, Option::LEVEL_ADVANCED)
.set_default("")
- .set_description("Base URL to the Vault server.")
+ .set_description("Vault server base address.")
.add_see_also({
"rgw_crypt_s3_kms_backend",
"rgw_crypt_vault_auth",
- "rgw_crypt_vault_token_file"}),
+ "rgw_crypt_vault_prefix"}),
+
+ Option("rgw_crypt_vault_prefix", Option::TYPE_STR, Option::LEVEL_ADVANCED)
+ .set_default("")
+ .set_description("Vault secret URL prefix, which can be used to restrict "
+ "access to a particular subset of the Vault secret space.")
+ .add_see_also({
+ "rgw_crypt_s3_kms_backend",
+ "rgw_crypt_vault_addr",
+ "rgw_crypt_vault_auth"}),
Option("rgw_crypt_suppress_logs", Option::TYPE_BOOL, Option::LEVEL_ADVANCED)
.set_default(true)
* Server-side encryption integrations with Key Management Systems (SSE-KMS)
*/
+#include <sys/stat.h>
#include "include/str_map.h"
#include "common/safe_io.h"
#include "rgw/rgw_crypt.h"
return m;
}
+/**
+ * Construct a full URL string by concatenating a "base" URL with another path,
+ * ensuring there is one and only one forward slash between them. If path is
+ * empty, the URL is not changed.
+ */
+static void concat_url(std::string &url, std::string path) {
+ if (!path.empty()) {
+ if (url.back() == '/' && path.front() == '/') {
+ url.pop_back();
+ } else if (url.back() != '/' && path.front() != '/') {
+ url.push_back('/');
+ }
+ url.append(path);
+ }
+}
+
static int get_actual_key_from_conf(CephContext *cct,
boost::string_view key_id,
boost::string_view key_selector,
return res;
}
-static int get_barbican_url(CephContext * const cct,
- std::string& url)
-{
- url = cct->_conf->rgw_barbican_url;
- if (url.empty()) {
- ldout(cct, 0) << "ERROR: conf rgw_barbican_url is not set" << dendl;
- return -EINVAL;
- }
-
- if (url.back() != '/') {
- url.append("/");
- }
-
- return 0;
-}
-
static int request_key_from_barbican(CephContext *cct,
boost::string_view key_id,
const std::string& barbican_token,
std::string& actual_key) {
- std::string secret_url;
int res;
- res = get_barbican_url(cct, secret_url);
- if (res < 0) {
- return res;
+
+ std::string secret_url = cct->_conf->rgw_barbican_url;
+ if (secret_url.empty()) {
+ ldout(cct, 0) << "ERROR: conf rgw_barbican_url is not set" << dendl;
+ return -EINVAL;
}
- secret_url += "v1/secrets/" + std::string(key_id);
+ concat_url(secret_url, "/v1/secrets/");
+ concat_url(secret_url, std::string(key_id));
bufferlist secret_bl;
RGWHTTPTransceiver secret_req(cct, "GET", secret_url, &secret_bl);
boost::string_view key_id,
bufferlist *secret_bl)
{
- std::string token_file, vault_addr, vault_token;
+ std::string token_file, secret_url, vault_token;
int res = 0;
token_file = cct->_conf->rgw_crypt_vault_token_file;
}
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 (-ENOENT == res) {
- ldout(cct, 0) << "ERROR: Token file '" << token_file << "' not found " << dendl;
- } else if (-EACCES == res) {
- ldout(cct, 0) << "ERROR: Permission denied reading token file" << dendl;
+ if (-EACCES == res) {
+ ldout(cct, 0) << "ERROR: Permission denied reading Vault token file" << dendl;
} else {
- ldout(cct, 0) << "ERROR: Failed to read token file with error " << res << dendl;
+ ldout(cct, 0) << "ERROR: Failed to read Vault token file with error " << res << dendl;
}
return res;
}
vault_token = std::string{buf, static_cast<size_t>(res)};
memset(buf, 0, sizeof(buf));
- vault_addr = cct->_conf->rgw_crypt_vault_addr;
- if (vault_addr.empty()) {
+ 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));
- std::string secret_url = vault_addr + 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');
-ENOENT
);
}
+
+TEST(TestSSEKMS, concat_url)
+{
+ // Each test has 3 strings:
+ // * the base URL
+ // * the path we want to concatenate
+ // * the exepected final URL
+ std::string tests[9][3] ={
+ {"", "", ""},
+ {"", "bar", "/bar"},
+ {"", "/bar", "/bar"},
+ {"foo", "", "foo"},
+ {"foo", "bar", "foo/bar"},
+ {"foo", "/bar", "foo/bar"},
+ {"foo/", "", "foo/"},
+ {"foo/", "bar", "foo/bar"},
+ {"foo/", "/bar", "foo/bar"},
+ };
+ for (const auto &test: tests) {
+ std::string url(test[0]), path(test[1]), expected(test[2]);
+ concat_url(url, path);
+ ASSERT_EQ(url, expected);
+ }
+}
\ No newline at end of file
; rgw crypt s3 kms backend = vault
; rgw crypt vault auth = token
; rgw crypt vault addr = http://127.0.0.1:8200
- ; rgw crypt vault token file = /path/to/token.file
+ ; rgw crypt vault prefix = /v1/secret/data
+ ; rgw crypt vault token file = $CEPH_CONF_PATH/vault.token
$extra_conf
EOF