Support storing image encryption keys in Vault

This commit is contained in:
Vitaliy Filippov
2026-03-20 21:00:32 +03:00
parent 76042d9ea8
commit 9d2a859760
15 changed files with 594 additions and 213 deletions
+2 -1
View File
@@ -135,6 +135,7 @@ Create an image. Options:
If parent is not a snapshot, it must be a read-only image. If parent is not a snapshot, it must be a read-only image.
* `--enc-key random` - Generate a new random AES-256-XTS encryption key for the new image. * `--enc-key random` - Generate a new random AES-256-XTS encryption key for the new image.
* `--enc-key HEX` - Set a specified AES-256-XTS key (64 bytes in hex) for the new image. * `--enc-key HEX` - Set a specified AES-256-XTS key (64 bytes in hex) for the new image.
* `--enc-key vault:ID` - Use an encryption key from an external Vault secret with specified ID.
``` ```
vitastor-cli create --snapshot <snapshot> [OPTIONS] <image> vitastor-cli create --snapshot <snapshot> [OPTIONS] <image>
@@ -147,7 +148,7 @@ Options:
* `-p|--pool POOL` - Move image to pool POOL, leaving the snapshot in the old pool. * `-p|--pool POOL` - Move image to pool POOL, leaving the snapshot in the old pool.
* `--enc-key random` - Change image encryption key to a new random AES-256-XTS key. * `--enc-key random` - Change image encryption key to a new random AES-256-XTS key.
* `--enc-key HEX` - Change image encryption key to a specified key or to an empty key. * `--enc-key KEY` - Change image encryption key to a specified key, Vault key or to an empty key.
By default, the image retains its old encryption key when taking a snapshot. By default, the image retains its old encryption key when taking a snapshot.
See also about [how to export snapshots](qemu.en.md#exporting-snapshots). See also about [how to export snapshots](qemu.en.md#exporting-snapshots).
+2 -1
View File
@@ -137,6 +137,7 @@ bench-kaveri kaveri 10 G 10 G 0 B/s 0 0 0 us 0 B/s 0
Если `PARENT` - не снимок, он должен быть помечен как образ только для чтения. Если `PARENT` - не снимок, он должен быть помечен как образ только для чтения.
* `--enc-key random` - Сгенерировать случайный ключ шифрования AES-256-XTS для нового образа. * `--enc-key random` - Сгенерировать случайный ключ шифрования AES-256-XTS для нового образа.
* `--enc-key HEX` - Установить заданный ключ AES-256-XTS (64 байта в hex) для нового образа. * `--enc-key HEX` - Установить заданный ключ AES-256-XTS (64 байта в hex) для нового образа.
* `--enc-key vault:ID` - Использовать ключ из внешнего секрета с заданным ID из Vault.
``` ```
vitastor-cli create --snapshot <snapshot> [ОПЦИИ] <image> vitastor-cli create --snapshot <snapshot> [ОПЦИИ] <image>
@@ -150,7 +151,7 @@ vitastor-cli snap-create [ОПЦИИ] <image>@<snapshot>
* `-p|--pool POOL` - Переместить образ в пул POOL, оставив снимок в старом пуле. * `-p|--pool POOL` - Переместить образ в пул POOL, оставив снимок в старом пуле.
* `--enc-key random` - Изменить ключ шифрования образа на новый случайный ключ AES-256-XTS. * `--enc-key random` - Изменить ключ шифрования образа на новый случайный ключ AES-256-XTS.
* `--enc-key HEX` - Изменить ключ шифрования образа на заданный или пустой ключ. * `--enc-key KEY` - Изменить ключ шифрования образа на заданный ключ, ключ из Vault или пустой ключ.
По умолчанию шифрованные образы сохраняют старый ключ при снятии снимка. По умолчанию шифрованные образы сохраняют старый ключ при снятии снимка.
Смотрите также информацию о том, [как экспортировать снимки](qemu.ru.md#экспорт-снимков). Смотрите также информацию о том, [как экспортировать снимки](qemu.ru.md#экспорт-снимков).
+2 -1
View File
@@ -24,6 +24,7 @@ add_library(vitastor_client SHARED
cluster_client.cpp cluster_client.cpp
cluster_client_list.cpp cluster_client_list.cpp
cluster_client_wb.cpp cluster_client_wb.cpp
cluster_client_icache.cpp
vitastor_c.cpp vitastor_c.cpp
) )
set_target_properties(vitastor_client PROPERTIES PUBLIC_HEADER "client/vitastor_c.h") set_target_properties(vitastor_client PROPERTIES PUBLIC_HEADER "client/vitastor_c.h")
@@ -99,7 +100,7 @@ endif (${WITH_QEMU})
add_executable(test_cluster_client add_executable(test_cluster_client
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
../test/test_cluster_client.cpp ../test/test_cluster_client.cpp
pg_states.cpp osd_ops.cpp cluster_client.cpp cluster_client_list.cpp cluster_client_wb.cpp msgr_op.cpp ../test/mock/messenger.cpp msgr_stop.cpp msgr_encrypt.cpp pg_states.cpp osd_ops.cpp cluster_client.cpp cluster_client_list.cpp cluster_client_wb.cpp cluster_client_icache.cpp msgr_op.cpp ../test/mock/messenger.cpp msgr_stop.cpp msgr_encrypt.cpp
etcd_state_client.cpp ../util/timerfd_manager.cpp ../util/addr_util.cpp ../util/str_util.cpp ../util/json_util.cpp ../../json11/json11.cpp etcd_state_client.cpp ../util/timerfd_manager.cpp ../util/addr_util.cpp ../util/str_util.cpp ../util/json_util.cpp ../../json11/json11.cpp
) )
target_link_libraries(test_cluster_client ${OPENSSL_LIBRARIES}) target_link_libraries(test_cluster_client ${OPENSSL_LIBRARIES})
+24 -141
View File
@@ -11,16 +11,6 @@
#define TRY_SEND_CONNECTING 1 #define TRY_SEND_CONNECTING 1
#define TRY_SEND_OK 2 #define TRY_SEND_OK 2
inode_cache_t::~inode_cache_t()
{
if (key_data)
{
free(key_data);
key_data = NULL;
op_enc = NULL;
}
}
cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd, json11::Json config) cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd, json11::Json config)
{ {
wb = new writeback_cache_t(); wb = new writeback_cache_t();
@@ -88,6 +78,7 @@ cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd
cluster_client_t::~cluster_client_t() cluster_client_t::~cluster_client_t()
{ {
vault_destroy();
if (retry_timeout_id >= 0) if (retry_timeout_id >= 0)
{ {
tfd->clear_timer(retry_timeout_id); tfd->clear_timer(retry_timeout_id);
@@ -492,6 +483,8 @@ void cluster_client_t::on_load_config_hook(json11::Json::object & etcd_global_co
self_tree_metrics.clear(); self_tree_metrics.clear();
client_hostname = new_hostname; client_hostname = new_hostname;
} }
// vault
vault_parse_config();
msgr.parse_config(config); msgr.parse_config(config);
st_cli.parse_config(config); st_cli.parse_config(config);
st_cli.load_pgs(); st_cli.load_pgs();
@@ -620,6 +613,7 @@ void cluster_client_t::on_change_pool_config_hook()
} }
inode_cache.clear(); inode_cache.clear();
inode_cache_children.clear(); inode_cache_children.clear();
vault_keys.clear();
continue_ops(); continue_ops();
} }
@@ -665,136 +659,6 @@ void cluster_client_t::on_change_node_placement_hook()
self_tree_metrics.clear(); self_tree_metrics.clear();
} }
// FIXME: Rework client API by adding open/close and cache inode information in the "FD" (maybe)
void cluster_client_t::on_change_inode_hook(uint64_t inode, bool removed)
{
std::vector<inode_t> children = { inode };
for (size_t i = 0; i < children.size(); i++)
{
auto it = inode_cache_children.lower_bound(std::make_pair(children[i], (inode_t)0));
while (it != inode_cache_children.end() && it->first == children[i])
{
children.push_back(it->second);
it++;
}
}
for (auto & inode: children)
{
auto it = inode_cache.find(inode);
if (it != inode_cache.end())
{
auto icache = it->second;
for (auto & parent: icache->chain)
{
inode_cache_children.erase(std::make_pair(parent, inode));
}
inode_cache.erase(it);
}
}
}
std::shared_ptr<inode_cache_t> cluster_client_t::inode_cache_get(inode_t ino)
{
auto icache_it = inode_cache.find(ino);
if (icache_it != inode_cache.end())
{
return icache_it->second;
}
// Fill inode cache
auto ino_it = st_cli.inode_config.find(ino);
if (ino_it == st_cli.inode_config.end())
{
inode_cache[ino] = NULL;
return NULL;
}
auto pool_it = st_cli.pool_config.find(INODE_POOL(ino));
if (pool_it == st_cli.pool_config.end())
{
inode_cache[ino] = NULL;
return NULL;
}
auto & inode_cfg = ino_it->second;
auto & pool_cfg = pool_it->second;
std::shared_ptr<inode_cache_t> icache = std::make_shared<inode_cache_t>();
icache->readonly = inode_cfg.readonly;
icache->chain.push_back(ino);
std::vector<inode_config_t*> chain_cfg;
int enc_key_count = !inode_cfg.enc_key.empty() ? 1 : 0;
if (inode_cfg.parent_id)
{
// Check for loops and cache the chain
robin_hood::unordered_flat_set<inode_t> seen;
seen.insert(ino);
uint64_t parent_id = inode_cfg.parent_id;
while (parent_id)
{
if (seen.find(parent_id) != seen.end())
{
icache->has_parent_loop = true;
break;
}
seen.insert(parent_id);
ino_it = st_cli.inode_config.find(parent_id);
if (INODE_POOL(parent_id) == INODE_POOL(ino))
{
icache->chain.push_back(parent_id);
if (ino_it == st_cli.inode_config.end())
chain_cfg.push_back(NULL);
else
{
chain_cfg.push_back(&ino_it->second);
if (!ino_it->second.enc_key.empty())
enc_key_count++;
}
}
else if (!icache->other_pool_parent_id)
icache->other_pool_parent_id = parent_id;
if (ino_it == st_cli.inode_config.end())
break;
parent_id = ino_it->second.parent_id;
}
}
// Generate encryption key chain, if applicable
if (enc_key_count)
{
uint8_t *key_data = (uint8_t*)malloc_or_die(
AES_256_XTS_KEY_SIZE * enc_key_count +
sizeof(uint8_t*) * icache->chain.size() +
sizeof(osd_op_enc_t)
);
uint8_t **keys = (uint8_t**)(key_data + AES_256_XTS_KEY_SIZE * enc_key_count);
osd_op_enc_t *enc = (osd_op_enc_t*)((uint8_t*)keys + sizeof(uint8_t*)*icache->chain.size());
size_t key_pos = 0;
for (size_t i = 0; i <= chain_cfg.size(); i++)
{
inode_config_t *cfg = !i ? &inode_cfg : chain_cfg[i-1];
if (cfg && !cfg->enc_key.empty())
{
assert(key_pos < AES_256_XTS_KEY_SIZE * enc_key_count);
assert(cfg->enc_key.size() == AES_256_XTS_KEY_SIZE);
keys[i] = key_data + key_pos;
memcpy(key_data + key_pos, cfg->enc_key.data(), AES_256_XTS_KEY_SIZE);
key_pos += AES_256_XTS_KEY_SIZE;
}
else
keys[i] = NULL;
}
enc->key_chain = keys;
enc->chain_size = icache->chain.size();
enc->read_chain_bitmap_pos = pool_cfg.data_block_size/pool_cfg.bitmap_granularity/8;
enc->bitmap_granularity = pool_cfg.bitmap_granularity;
icache->key_data = key_data;
icache->op_enc = enc;
}
inode_cache[ino] = icache;
for (auto & parent: icache->chain)
{
if (parent != ino)
inode_cache_children.insert(std::make_pair(parent, ino));
}
return icache;
}
bool cluster_client_t::is_ready() bool cluster_client_t::is_ready()
{ {
return pgs_loaded; return pgs_loaded;
@@ -816,6 +680,10 @@ bool cluster_client_t::flush()
{ {
if (!ringloop) if (!ringloop)
{ {
if (vault_loading)
{
return false;
}
if (wb->writeback_queue.size()) if (wb->writeback_queue.size())
{ {
wb->start_writebacks(this, 0); wb->start_writebacks(this, 0);
@@ -838,7 +706,7 @@ bool cluster_client_t::flush()
sync_done = true; sync_done = true;
}; };
execute(sync); execute(sync);
while (!sync_done) while (!sync_done || vault_loading)
{ {
ringloop->loop(); ringloop->loop();
if (!sync_done) if (!sync_done)
@@ -1163,6 +1031,21 @@ bool cluster_client_t::check_rw(cluster_op_t *op)
} }
} }
} }
if (icache && icache->err_code)
{
if (icache->err_code == EPERM)
{
op->retval = -EPERM;
auto cb = std::move(op->callback);
cb(op);
return false;
}
else if (icache->err_code == EAGAIN)
{
key_wait_ops.push_back(op);
return false;
}
}
return true; return true;
} }
+38 -4
View File
@@ -90,10 +90,17 @@ struct inode_cache_t
bool readonly = false; bool readonly = false;
bool has_parent_loop = false; bool has_parent_loop = false;
inode_t other_pool_parent_id = 0; inode_t other_pool_parent_id = 0;
int err_code = 0;
~inode_cache_t(); ~inode_cache_t();
}; };
struct vault_load_key_t
{
int key_state = 0;
std::string key;
};
// FIXME: Split into public and private interfaces // FIXME: Split into public and private interfaces
class __attribute__((visibility("default"))) cluster_client_t class __attribute__((visibility("default"))) cluster_client_t
{ {
@@ -103,8 +110,8 @@ public:
timerfd_manager_t *tfd = NULL; timerfd_manager_t *tfd = NULL;
ring_loop_t *ringloop = NULL; ring_loop_t *ringloop = NULL;
std::map<pool_id_t, uint64_t> pg_counts; // config:
std::map<pool_pg_num_t, osd_num_t> pg_primary;
// client_max_dirty_* is actually "max unsynced", for the case when immediate_commit is off // client_max_dirty_* is actually "max unsynced", for the case when immediate_commit is off
uint64_t client_max_dirty_bytes = 0; uint64_t client_max_dirty_bytes = 0;
uint64_t client_max_dirty_ops = 0; uint64_t client_max_dirty_ops = 0;
@@ -116,12 +123,23 @@ public:
uint64_t client_max_writeback_iodepth = 0; uint64_t client_max_writeback_iodepth = 0;
std::string conf_hostname; std::string conf_hostname;
std::string vault_url;
std::string vault_client_cert;
std::string vault_client_key;
std::string vault_ca;
std::string vault_secret_api_path;
uint64_t vault_timeout_ms = 0;
uint64_t vault_error_timeout_sec = 0;
uint64_t vault_refresh_leeway_sec = 0;
int log_level = 0; int log_level = 0;
int client_retry_interval = 50; // ms int client_retry_interval = 50; // ms
int client_eio_retry_interval = 1000; // ms int client_eio_retry_interval = 1000; // ms
bool client_retry_enospc = true; bool client_retry_enospc = true;
int client_wait_up_timeout = 16; // sec (for listings) int client_wait_up_timeout = 16; // sec (for listings)
// state:
std::string client_hostname; std::string client_hostname;
std::map<std::string, int> self_tree_metrics; std::map<std::string, int> self_tree_metrics;
std::map<osd_num_t, int> osd_tree_metrics; std::map<osd_num_t, int> osd_tree_metrics;
@@ -129,6 +147,7 @@ public:
int retry_timeout_id = -1; int retry_timeout_id = -1;
int retry_timeout_duration = 0; int retry_timeout_duration = 0;
std::vector<cluster_op_t*> offline_ops; std::vector<cluster_op_t*> offline_ops;
std::vector<cluster_op_t*> key_wait_ops;
cluster_op_t *op_queue_head = NULL, *op_queue_tail = NULL; cluster_op_t *op_queue_head = NULL, *op_queue_tail = NULL;
writeback_cache_t *wb = NULL; writeback_cache_t *wb = NULL;
std::set<osd_num_t> dirty_osds; std::set<osd_num_t> dirty_osds;
@@ -142,7 +161,17 @@ public:
robin_hood::unordered_flat_map<inode_t, std::shared_ptr<inode_cache_t>> inode_cache; robin_hood::unordered_flat_map<inode_t, std::shared_ptr<inode_cache_t>> inode_cache;
std::set<std::pair<inode_t, inode_t>> inode_cache_children; std::set<std::pair<inode_t, inode_t>> inode_cache_children;
http_context_t *vault_http_ctx = NULL;
http_co_t *vault_http_cli = NULL;
bool vault_loading = false;
std::string vault_token;
bool vault_auth_error = false;
timespec vault_token_expire = {};
std::vector<std::string> vault_key_load_queue;
std::map<std::string, vault_load_key_t> vault_keys;
bool pgs_loaded = false; bool pgs_loaded = false;
std::map<pool_id_t, uint64_t> pg_counts;
ring_consumer_t consumer; ring_consumer_t consumer;
std::vector<std::function<void(void)>> on_ready_hooks; std::vector<std::function<void(void)>> on_ready_hooks;
int list_retry_timeout_id = -1; int list_retry_timeout_id = -1;
@@ -182,6 +211,13 @@ protected:
#endif #endif
void continue_ops(int time_passed = 0); void continue_ops(int time_passed = 0);
std::shared_ptr<inode_cache_t> inode_cache_get(inode_t ino);
void vault_parse_config();
bool vault_check_token();
void vault_load_keys();
void vault_destroy();
void vault_parse_secret(const std::string & key_id, const std::string & err, json11::Json data);
protected: protected:
bool affects_osd(uint64_t inode, uint64_t offset, uint64_t len, osd_num_t osd); bool affects_osd(uint64_t inode, uint64_t offset, uint64_t len, osd_num_t osd);
bool affects_pg(uint64_t inode, uint64_t offset, uint64_t len, pool_id_t pool_id, pg_num_t pg_num); bool affects_pg(uint64_t inode, uint64_t offset, uint64_t len, pool_id_t pool_id, pg_num_t pg_num);
@@ -194,8 +230,6 @@ protected:
void on_change_node_placement_hook(); void on_change_node_placement_hook();
void on_change_inode_hook(uint64_t inode, bool removed); void on_change_inode_hook(uint64_t inode, bool removed);
std::shared_ptr<inode_cache_t> inode_cache_get(inode_t ino);
void execute_internal(cluster_op_t *op); void execute_internal(cluster_op_t *op);
void execute_cas(cluster_op_t *op); void execute_cas(cluster_op_t *op);
void unshift_op(cluster_op_t *op); void unshift_op(cluster_op_t *op);
+366
View File
@@ -0,0 +1,366 @@
// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details)
#include <stdexcept>
#include <assert.h>
#include "cluster_client_impl.h"
#include "http_client.h"
#include "str_util.h"
#define VAULT_KEY_NOT_LOADED 0
#define VAULT_KEY_LOADING 1
#define VAULT_KEY_LOADED 2
#define VAULT_KEY_ERROR 3
inode_cache_t::~inode_cache_t()
{
if (key_data)
{
free(key_data);
key_data = NULL;
op_enc = NULL;
}
}
void cluster_client_t::vault_destroy()
{
if (vault_http_ctx)
{
#ifndef __MOCK__
http_destroy(vault_http_cli);
http_context_destroy(vault_http_ctx);
vault_http_cli = NULL;
vault_http_ctx = NULL;
#endif
}
}
void cluster_client_t::vault_parse_config()
{
vault_url = config["vault_url"].string_value();
vault_client_cert = config["vault_client_cert"].string_value();
vault_client_key = config["vault_client_key"].string_value();
vault_ca = config["vault_ca"].string_value();
vault_secret_api_path = "/v1/secret/";
if (config["vault_secret_api_path"].is_string())
vault_secret_api_path = config["vault_secret_api_path"].string_value();
vault_timeout_ms = config["vault_timeout_ms"].uint64_value();
if (!vault_timeout_ms)
vault_timeout_ms = 5000;
vault_error_timeout_sec = config["vault_error_timeout_sec"].uint64_value();
if (!vault_error_timeout_sec)
vault_error_timeout_sec = 60;
vault_refresh_leeway_sec = config["vault_refresh_leeway_sec"].uint64_value();
if (!vault_refresh_leeway_sec)
vault_refresh_leeway_sec = 60;
}
// FIXME: Rework client API by adding open/close and cache inode information in the "FD" (maybe)
void cluster_client_t::on_change_inode_hook(uint64_t inode, bool removed)
{
std::vector<inode_t> children = { inode };
for (size_t i = 0; i < children.size(); i++)
{
auto it = inode_cache_children.lower_bound(std::make_pair(children[i], (inode_t)0));
while (it != inode_cache_children.end() && it->first == children[i])
{
children.push_back(it->second);
it++;
}
}
for (auto & inode: children)
{
auto it = inode_cache.find(inode);
if (it != inode_cache.end())
{
auto icache = it->second;
for (auto & parent: icache->chain)
{
inode_cache_children.erase(std::make_pair(parent, inode));
}
inode_cache.erase(it);
}
}
}
std::shared_ptr<inode_cache_t> cluster_client_t::inode_cache_get(inode_t ino)
{
auto icache_it = inode_cache.find(ino);
if (icache_it != inode_cache.end())
{
return icache_it->second;
}
// Fill inode cache
auto ino_it = st_cli.inode_config.find(ino);
if (ino_it == st_cli.inode_config.end())
{
inode_cache[ino] = NULL;
return NULL;
}
auto pool_it = st_cli.pool_config.find(INODE_POOL(ino));
if (pool_it == st_cli.pool_config.end())
{
inode_cache[ino] = NULL;
return NULL;
}
auto & inode_cfg = ino_it->second;
auto & pool_cfg = pool_it->second;
std::shared_ptr<inode_cache_t> icache = std::make_shared<inode_cache_t>();
icache->readonly = inode_cfg.readonly;
icache->chain.push_back(ino);
std::vector<inode_config_t*> chain_cfg;
int enc_key_count = !inode_cfg.enc_key.empty() ? 1 : 0;
if (inode_cfg.parent_id)
{
// Check for loops and cache the chain
robin_hood::unordered_flat_set<inode_t> seen;
seen.insert(ino);
uint64_t parent_id = inode_cfg.parent_id;
while (parent_id)
{
if (seen.find(parent_id) != seen.end())
{
icache->has_parent_loop = true;
break;
}
seen.insert(parent_id);
ino_it = st_cli.inode_config.find(parent_id);
if (INODE_POOL(parent_id) == INODE_POOL(ino))
{
icache->chain.push_back(parent_id);
if (ino_it == st_cli.inode_config.end())
chain_cfg.push_back(NULL);
else
{
chain_cfg.push_back(&ino_it->second);
if (!ino_it->second.enc_key.empty())
enc_key_count++;
}
}
else if (!icache->other_pool_parent_id)
icache->other_pool_parent_id = parent_id;
if (ino_it == st_cli.inode_config.end())
break;
parent_id = ino_it->second.parent_id;
}
}
// Check external keys and wait for loading, if required
if (enc_key_count)
{
for (size_t i = 0; i <= chain_cfg.size(); i++)
{
inode_config_t *cfg = !i ? &inode_cfg : chain_cfg[i-1];
if (cfg && cfg->enc_key.substr(0, strlen(VAULT_KEY_PREFIX)) == VAULT_KEY_PREFIX)
{
auto & ik = vault_keys[inode_cfg.enc_key];
if (ik.key_state == VAULT_KEY_ERROR || vault_url.empty())
{
icache->err_code = EPERM;
enc_key_count = 0;
}
else if (ik.key_state == VAULT_KEY_NOT_LOADED)
{
ik.key_state = VAULT_KEY_LOADING;
vault_key_load_queue.push_back(inode_cfg.enc_key);
vault_load_keys();
icache->err_code = EAGAIN;
enc_key_count = 0;
}
else if (ik.key_state == VAULT_KEY_LOADING)
{
icache->err_code = EAGAIN;
enc_key_count = 0;
}
else
{
assert(ik.key_state == VAULT_KEY_LOADED);
}
}
}
}
// Generate encryption key chain, if applicable
if (enc_key_count)
{
uint8_t *key_data = (uint8_t*)malloc_or_die(
AES_256_XTS_KEY_SIZE * enc_key_count +
sizeof(uint8_t*) * icache->chain.size() +
sizeof(osd_op_enc_t)
);
uint8_t **keys = (uint8_t**)(key_data + AES_256_XTS_KEY_SIZE * enc_key_count);
osd_op_enc_t *enc = (osd_op_enc_t*)((uint8_t*)keys + sizeof(uint8_t*)*icache->chain.size());
size_t key_pos = 0;
for (size_t i = 0; i <= chain_cfg.size(); i++)
{
inode_config_t *cfg = !i ? &inode_cfg : chain_cfg[i-1];
if (cfg && !cfg->enc_key.empty())
{
const auto & key = cfg->enc_key.substr(0, strlen(VAULT_KEY_PREFIX)) == VAULT_KEY_PREFIX
? vault_keys.at(cfg->enc_key).key
: cfg->enc_key;
assert(key_pos < AES_256_XTS_KEY_SIZE * enc_key_count);
assert(key.size() == 2*AES_256_XTS_KEY_SIZE);
keys[i] = key_data + key_pos;
fromhexstr(key, AES_256_XTS_KEY_SIZE, key_data + key_pos);
key_pos += AES_256_XTS_KEY_SIZE;
}
else
keys[i] = NULL;
}
enc->key_chain = keys;
enc->chain_size = icache->chain.size();
enc->read_chain_bitmap_pos = pool_cfg.data_block_size/pool_cfg.bitmap_granularity/8;
enc->bitmap_granularity = pool_cfg.bitmap_granularity;
icache->key_data = key_data;
icache->op_enc = enc;
}
inode_cache[ino] = icache;
for (auto & parent: icache->chain)
{
if (parent != ino)
inode_cache_children.insert(std::make_pair(parent, ino));
}
return icache;
}
#ifndef __MOCK__
bool cluster_client_t::vault_check_token()
{
timespec now;
clock_gettime(CLOCK_REALTIME, &now);
if (!vault_token_expire.tv_sec || vault_token_expire.tv_sec < now.tv_sec)
{
vault_loading = true;
http_json_post(
vault_http_cli, vault_url+"/v1/auth/cert/login", json11::Json::object{}, "",
(http_options_t){ .timeout = (int)vault_timeout_ms, .keepalive = true },
[this](http_message_t *response)
{
clock_gettime(CLOCK_REALTIME, &vault_token_expire);
vault_loading = false;
std::string err;
json11::Json data;
response->parse_json_response(err, data);
if (err != "")
{
vault_token_expire.tv_sec += vault_error_timeout_sec;
fprintf(stderr, "Vault request failed: %s\n", err.c_str());
}
else
{
uint64_t ttl = data["auth"]["lease_duration"].uint64_value();
vault_token = data["auth"]["client_token"].string_value();
if (vault_token.empty() || !ttl)
{
vault_token_expire.tv_sec += vault_error_timeout_sec;
fprintf(stderr, "No token or lease_duration in Vault response: %s\n", data.dump().c_str());
}
else
{
if (ttl < vault_refresh_leeway_sec)
vault_token_expire.tv_sec += ttl/2;
else
vault_token_expire.tv_sec += ttl - vault_refresh_leeway_sec;
}
}
vault_load_keys();
}
);
return false;
}
if (vault_token.empty())
{
// Auth error happened, mark all loads as failed
for (auto & key_id: vault_key_load_queue)
{
auto & k = vault_keys[key_id];
k.key_state = VAULT_KEY_ERROR;
}
vault_key_load_queue.clear();
auto ops = std::move(key_wait_ops);
for (cluster_op_t *op: ops)
inode_cache.erase(op->inode);
for (cluster_op_t *op: ops)
execute_internal(op);
return false;
}
return true;
}
#endif
void cluster_client_t::vault_load_keys()
{
if (vault_loading || !vault_key_load_queue.size())
{
return;
}
#ifdef __MOCK__
vault_loading = true;
#else
if (!vault_http_ctx)
{
std::string error;
vault_http_ctx = http_context_init(tfd, vault_client_cert, vault_client_key, vault_ca, true, error);
if (!vault_http_ctx)
{
fprintf(stderr, "Failed to initialize HTTP context for Vault: %s\n", error.c_str());
exit(1);
}
vault_http_cli = http_init(vault_http_ctx);
}
if (!vault_check_token())
{
return;
}
std::string key_id = vault_key_load_queue[0];
vault_key_load_queue.erase(vault_key_load_queue.begin());
vault_loading = true;
http_get(
vault_http_cli, vault_url+vault_secret_api_path+key_id.substr(strlen(VAULT_KEY_PREFIX)), "X-Vault-Token: "+vault_token+"\r\n",
(http_options_t){ .timeout = (int)vault_timeout_ms, .keepalive = true },
[this, key_id](http_message_t *response)
{
vault_loading = false;
std::string err;
json11::Json data;
response->parse_json_response(err, data);
vault_parse_secret(key_id, err, data);
}
);
#endif
}
void cluster_client_t::vault_parse_secret(const std::string & key_id, const std::string & err, json11::Json data)
{
vault_loading = false;
auto & k = vault_keys[key_id];
if (err != "")
{
k.key_state = VAULT_KEY_ERROR;
fprintf(stderr, "Vault %s%s%s request failed: %s\n", vault_url.c_str(),
vault_secret_api_path.c_str(), key_id.c_str()+strlen(VAULT_KEY_PREFIX), err.c_str());
}
else
{
auto hexkey = data["data"]["key"].string_value();
if (hexkey.empty() || !ishexstr(hexkey) || hexkey.size() != 2*AES_256_XTS_KEY_SIZE)
{
k.key_state = VAULT_KEY_ERROR;
fprintf(stderr, "Vault /v1/secret/%s request failed: 'key' is empty or has invalid format\n", key_id.c_str());
}
else
{
k.key_state = VAULT_KEY_LOADED;
k.key = hexkey;
}
}
if (vault_key_load_queue.empty())
{
auto ops = std::move(key_wait_ops);
for (cluster_op_t *op: ops)
inode_cache.erase(op->inode);
for (cluster_op_t *op: ops)
execute_internal(op);
}
else
vault_load_keys();
}
+9 -29
View File
@@ -98,32 +98,15 @@ http_context_t *etcd_state_client_t::get_http_ctx()
void etcd_state_client_t::etcd_call_oneshot(const std::string & etcd_url, const std::string & api, json11::Json payload, void etcd_state_client_t::etcd_call_oneshot(const std::string & etcd_url, const std::string & api, json11::Json payload,
int timeout, std::function<void(std::string, json11::Json)> callback) int timeout, std::function<void(std::string, json11::Json)> callback)
{ {
std::string etcd_api_path;
bool ssl = etcd_url.substr(0, 8) == "https://";
auto etcd_address = etcd_url.substr(ssl ? 8 : 7);
int pos = etcd_address.find('/');
if (pos >= 0)
{
etcd_api_path = etcd_address.substr(pos);
etcd_address = etcd_address.substr(0, pos);
}
std::string req = payload.dump();
req = "POST "+etcd_api_path+api+" HTTP/1.1\r\n"
"Host: "+etcd_address+"\r\n"
"Content-Type: application/json\r\n"
"Content-Length: "+std::to_string(req.size())+"\r\n"
"Connection: close\r\n"
"\r\n"+req;
auto http_cli = http_init(get_http_ctx()); auto http_cli = http_init(get_http_ctx());
auto cb = [http_cli, callback](http_message_t *response) http_json_post(http_cli, etcd_url+api, payload, "", { .timeout = timeout }, [http_cli, callback](http_message_t *response)
{ {
std::string err; std::string err;
json11::Json data; json11::Json data;
response->parse_json_response(err, data); response->parse_json_response(err, data);
callback(err, data); callback(err, data);
http_destroy(http_cli); http_destroy(http_cli);
}; });
http_request(http_cli, etcd_address, req, { .timeout = timeout, .ssl = ssl }, cb);
} }
void etcd_state_client_t::etcd_call(const std::string & api, json11::Json payload, int timeout, void etcd_state_client_t::etcd_call(const std::string & api, json11::Json payload, int timeout,
@@ -1448,7 +1431,7 @@ json11::Json::object etcd_state_client_t::serialize_inode_cfg(inode_config_t *cf
} }
if (!cfg->enc_key.empty()) if (!cfg->enc_key.empty())
{ {
new_cfg["enc_key"] = tohexstr(cfg->enc_key.data(), cfg->enc_key.size()); new_cfg["enc_key"] = cfg->enc_key;
} }
if (cfg->readonly) if (cfg->readonly)
{ {
@@ -1484,18 +1467,15 @@ inode_config_t etcd_state_client_t::deserialize_inode_cfg(uint64_t inode_num, js
else else
parent_inode_num |= parent_pool_id << (64-POOL_ID_BITS); parent_inode_num |= parent_pool_id << (64-POOL_ID_BITS);
} }
std::vector<uint8_t> enc_key; std::string enc_key;
if (!value["enc_key"].is_null()) if (!value["enc_key"].is_null())
{ {
if (value["enc_key"].string_value().size() == 2*AES_256_XTS_KEY_SIZE) enc_key = value["enc_key"].string_value();
if (enc_key.substr(0, strlen(VAULT_KEY_PREFIX)) != VAULT_KEY_PREFIX &&
(enc_key.size() != 2*AES_256_XTS_KEY_SIZE || !ishexstr(enc_key)))
{ {
enc_key.resize(AES_256_XTS_KEY_SIZE); enc_key = "";
if (fromhexstr(value["enc_key"].string_value(), AES_256_XTS_KEY_SIZE, enc_key.data()) < AES_256_XTS_KEY_SIZE) fprintf(stderr, "Inode %u/%ju has invalid enc_key, should be %u bit hex string or Vault key reference\n",
enc_key.clear();
}
if (enc_key.empty())
{
fprintf(stderr, "Inode %u/%ju has invalid enc_key, should be %u bit hex string\n",
INODE_POOL(inode_num), INODE_NO_POOL(inode_num), AES_256_XTS_KEY_SIZE); INODE_POOL(inode_num), INODE_NO_POOL(inode_num), AES_256_XTS_KEY_SIZE);
} }
} }
+3 -1
View File
@@ -20,6 +20,8 @@
#define MAX_DATA_BLOCK_SIZE 128*1024*1024 #define MAX_DATA_BLOCK_SIZE 128*1024*1024
#define DEFAULT_BITMAP_GRANULARITY 4096 #define DEFAULT_BITMAP_GRANULARITY 4096
#define VAULT_KEY_PREFIX "vault:"
#ifndef IMMEDIATE_NONE #ifndef IMMEDIATE_NONE
#define IMMEDIATE_NONE 0 #define IMMEDIATE_NONE 0
#define IMMEDIATE_SMALL 1 #define IMMEDIATE_SMALL 1
@@ -84,7 +86,7 @@ struct inode_config_t
inode_t parent_id = 0; inode_t parent_id = 0;
bool readonly = false; bool readonly = false;
bool deleted = false; bool deleted = false;
std::vector<uint8_t> enc_key; std::string enc_key;
// Arbitrary metadata // Arbitrary metadata
json11::Json meta; json11::Json meta;
// Change revision of the metadata in etcd // Change revision of the metadata in etcd
+42
View File
@@ -287,6 +287,48 @@ void http_request(http_co_t *handler, const std::string & host, const std::strin
handler->send_request(host, request, options, response_callback); handler->send_request(host, request, options, response_callback);
} }
void http_get(http_co_t *handler, const std::string & url, const std::string & headers,
const http_options_t & options, std::function<void(http_message_t *response)> response_callback)
{
std::string path;
auto ssl = url.substr(0, 8) == "https://";
auto host = url.substr(ssl ? 8 : 7);
auto pos = host.find('/');
if (pos != std::string::npos)
{
path = host.substr(pos);
host = host.substr(0, pos);
}
std::string req = "GET "+path+" HTTP/1.1\r\n"
"Host: "+host+"\r\n"
"Connection: "+(options.keepalive ? "keep-alive" : "close")+"\r\n"+
headers+"\r\n";
handler->send_request(host, req, { .timeout = options.timeout, .keepalive = options.keepalive, .ssl = ssl }, response_callback);
}
void http_json_post(http_co_t *handler, const std::string & url, json11::Json body, const std::string & headers,
const http_options_t & options, std::function<void(http_message_t *response)> response_callback)
{
std::string path;
auto ssl = url.substr(0, 8) == "https://";
auto host = url.substr(ssl ? 8 : 7);
auto pos = host.find('/');
if (pos != std::string::npos)
{
path = host.substr(pos);
host = host.substr(0, pos);
}
std::string req = body.dump();
req = "POST "+path+" HTTP/1.1\r\n"
"Host: "+host+"\r\n"
"Content-Type: application/json\r\n"
"Content-Length: "+std::to_string(req.size())+"\r\n"
"Connection: "+(options.keepalive ? "keep-alive" : "close")+"\r\n"+
headers+
"\r\n"+req;
handler->send_request(host, req, { .timeout = options.timeout, .keepalive = options.keepalive, .ssl = ssl }, response_callback);
}
void http_serve(http_co_t *handler, int peer_fd, const http_options_t & options, std::function<void(http_message_t *msg)> request_callback) void http_serve(http_co_t *handler, int peer_fd, const http_options_t & options, std::function<void(http_message_t *msg)> request_callback)
{ {
if (handler->state != HTTP_CO_SERVER || handler->peer_fd != peer_fd) if (handler->state != HTTP_CO_SERVER || handler->peer_fd != peer_fd)
+4
View File
@@ -56,6 +56,10 @@ void open_websocket(http_co_t *handler, const std::string & addr, const std::str
const http_options_t & options, std::function<void(http_message_t *msg)> on_message); const http_options_t & options, std::function<void(http_message_t *msg)> on_message);
void http_request(http_co_t *handler, const std::string & host, const std::string & request, void http_request(http_co_t *handler, const std::string & host, const std::string & request,
const http_options_t & options, std::function<void(http_message_t *response)> response_callback); const http_options_t & options, std::function<void(http_message_t *response)> response_callback);
void http_get(http_co_t *handler, const std::string & url, const std::string & headers,
const http_options_t & options, std::function<void(http_message_t *response)> response_callback);
void http_json_post(http_co_t *handler, const std::string & url, json11::Json body, const std::string & headers,
const http_options_t & options, std::function<void(http_message_t *response)> response_callback);
void http_post_message(http_co_t *handler, uint8_t type, const std::string & msg); void http_post_message(http_co_t *handler, uint8_t type, const std::string & msg);
void http_serve(http_co_t *handler, int peer_fd, const http_options_t & options, void http_serve(http_co_t *handler, int peer_fd, const http_options_t & options,
std::function<void(http_message_t *msg)> request_callback); std::function<void(http_message_t *msg)> request_callback);
+8 -7
View File
@@ -42,12 +42,13 @@ static const char* help_text =
"\n" "\n"
"vitastor-cli create -s|--size SIZE [OPTIONS] <name>\n" "vitastor-cli create -s|--size SIZE [OPTIONS] <name>\n"
" Create an image. Options:\n" " Create an image. Options:\n"
" -s|--size SIZE New image size in bytes or with a K/M/G/T unit suffix.\n" " -s|--size SIZE New image size in bytes or with a K/M/G/T unit suffix.\n"
" -p|--pool POOL Specify pool for the new image (may be omitted if there is only 1 pool).\n" " -p|--pool POOL Specify pool for the new image (may be omitted if there is only 1 pool).\n"
" --parent PARENT Create a copy-on-write image clone based on PARENT (or PARENT@SNAPSHOT).\n" " --parent PARENT Create a copy-on-write image clone based on PARENT (or PARENT@SNAPSHOT).\n"
" If parent is not a snapshot, it must be a read-only image.\n" " If parent is not a snapshot, it must be a read-only image.\n"
" --enc-key random Generate a new random AES-256-XTS encryption key for the new image.\n" " --enc-key random Generate a new random AES-256-XTS encryption key for the new image.\n"
" --enc-key HEX Set a specified AES-256-XTS key (64 bytes in hex) for the new image.\n" " --enc-key HEX Set a specified AES-256-XTS key (64 bytes in hex) for the new image.\n"
" --enc-key vault:ID Use an encryption key from an external Vault secret with specified ID.\n"
"\n" "\n"
"vitastor-cli create --snapshot <snapshot> [OPTIONS] <image>\n" "vitastor-cli create --snapshot <snapshot> [OPTIONS] <image>\n"
"vitastor-cli snap-create [OPTIONS] <image>@<snapshot>\n" "vitastor-cli snap-create [OPTIONS] <image>@<snapshot>\n"
@@ -55,7 +56,7 @@ static const char* help_text =
" Options:\n" " Options:\n"
" -p|--pool POOL Move image to pool POOL, leaving the snapshot in the old pool.\n" " -p|--pool POOL Move image to pool POOL, leaving the snapshot in the old pool.\n"
" --enc-key random Change image encryption key to a new random AES-256-XTS key.\n" " --enc-key random Change image encryption key to a new random AES-256-XTS key.\n"
" --enc-key HEX Change image encryption key to a specified key or to an empty key.\n" " --enc-key KEY Change image encryption key to a specified key, Vault key or to an empty key.\n"
" By default, the image retains its old key when taking a snapshot.\n" " By default, the image retains its old key when taking a snapshot.\n"
"\n" "\n"
"vitastor-cli modify <name> [--rename <new-name>] [--resize <size>] [--readonly | --readwrite] [-f|--force] [--down-ok]\n" "vitastor-cli modify <name> [--rename <new-name>] [--resize <size>] [--readonly | --readwrite] [-f|--force] [--down-ok]\n"
+17 -15
View File
@@ -456,11 +456,12 @@ resume_3:
}; };
if (set_key) if (set_key)
{ {
new_cfg.enc_key.resize(enc_key.size()/2); new_cfg.enc_key = enc_key;
fromhexstr(enc_key, new_cfg.enc_key.size(), new_cfg.enc_key.data());
} }
else if (new_snap != "") else if (new_snap != "")
{
new_cfg.enc_key = cur_cfg.enc_key; new_cfg.enc_key = cur_cfg.enc_key;
}
json11::Json::array checks = json11::Json::array { json11::Json::array checks = json11::Json::array {
json11::Json::object { json11::Json::object {
{ "target", "VERSION" }, { "target", "VERSION" },
@@ -600,19 +601,6 @@ std::function<bool(cli_result_t &)> cli_tool_t::start_create(json11::Json cfg)
if (!cfg["enc_key"].is_null()) if (!cfg["enc_key"].is_null())
{ {
image_creator->set_key = true; image_creator->set_key = true;
image_creator->enc_key = cfg["enc_key"].string_value();
if (image_creator->enc_key != "" &&
#ifdef WITH_OPENSSL
image_creator->enc_key != "random" &&
#endif
(!ishexstr(image_creator->enc_key) || image_creator->enc_key.size() != 128))
{
return [](cli_result_t & result)
{
result = (cli_result_t){ .err = EINVAL, .text = "Encryption key is not a 512-bit hex string, not \"\" and not \"random\"" };
return true;
};
}
#ifdef WITH_OPENSSL #ifdef WITH_OPENSSL
if (image_creator->enc_key == "random") if (image_creator->enc_key == "random")
{ {
@@ -621,6 +609,20 @@ std::function<bool(cli_result_t &)> cli_tool_t::start_create(json11::Json cfg)
image_creator->enc_key = tohexstr(newkey, 64); image_creator->enc_key = tohexstr(newkey, 64);
} }
#endif #endif
else
{
image_creator->enc_key = cfg["enc_key"].string_value();
if (image_creator->enc_key != "" &&
image_creator->enc_key.substr(0, strlen(VAULT_KEY_PREFIX)) != VAULT_KEY_PREFIX &&
(!ishexstr(image_creator->enc_key) || image_creator->enc_key.size() != 128))
{
return [](cli_result_t & result)
{
result = (cli_result_t){ .err = EINVAL, .text = "Encryption key is not a 512-bit hex string, not \"\" and not \"random\"" };
return true;
};
}
}
} }
image_creator->new_parent = cfg["parent"].string_value(); image_creator->new_parent = cfg["parent"].string_value();
if (!cfg["size"].is_null()) if (!cfg["size"].is_null())
+4 -3
View File
@@ -159,14 +159,15 @@ resume_1:
state = 100; state = 100;
return; return;
} }
if (enc_key != "" && (!ishexstr(enc_key) || enc_key.size() != 128)) if (enc_key != "" &&
enc_key.substr(0, strlen(VAULT_KEY_PREFIX)) != VAULT_KEY_PREFIX &&
(!ishexstr(enc_key) || enc_key.size() != 128))
{ {
result = (cli_result_t){ .err = EINVAL, .text = "Encryption key is not a 512-bit hex string and not \"\"" }; result = (cli_result_t){ .err = EINVAL, .text = "Encryption key is not a 512-bit hex string and not \"\"" };
state = 100; state = 100;
return; return;
} }
cfg.enc_key.resize(enc_key.size()/2); cfg.enc_key = enc_key;
fromhexstr(enc_key, cfg.enc_key.size(), cfg.enc_key.data());
} }
{ {
std::string cur_cfg_key = base64_encode(parent->cli->st_cli.etcd_prefix+ std::string cur_cfg_key = base64_encode(parent->cli->st_cli.etcd_prefix+
+2 -2
View File
@@ -106,7 +106,7 @@
"description": "Encryption key for the new/cloned image", "description": "Encryption key for the new/cloned image",
"oneOf": [ "oneOf": [
{ "type": "string", "enum": [ "", "random" ] }, { "type": "string", "enum": [ "", "random" ] },
{ "type": "string", "pattern": "^[0-9a-fA-F]{128}$" } { "type": "string", "pattern": "^[0-9a-fA-F]{128}$|^vault:" }
] ]
} }
} }
@@ -143,7 +143,7 @@
"description": "Change encryption key for the image", "description": "Change encryption key for the image",
"oneOf": [ "oneOf": [
{ "type": "string", "enum": [ "" ] }, { "type": "string", "enum": [ "" ] },
{ "type": "string", "pattern": "^[0-9a-fA-F]{128}$" } { "type": "string", "pattern": "^[0-9a-fA-F]{128}$|^vault:" }
] ]
}, },
"force": { "type": "boolean", "description": "Proceed with shrinking or setting readwrite flag even if the image has children" }, "force": { "type": "boolean", "description": "Proceed with shrinking or setting readwrite flag even if the image has children" },
+71 -8
View File
@@ -55,7 +55,7 @@ int *test_write(cluster_client_t *cli, uint64_t offset, uint64_t len, uint8_t c,
{ {
printf("Post write %jx+%jx\n", offset, len); printf("Post write %jx+%jx\n", offset, len);
int *r = new int; int *r = new int;
*r = instant ? -2 : -1; *r = instant ? -1001 : -1000;
cluster_op_t *op = new cluster_op_t(); cluster_op_t *op = new cluster_op_t();
op->opcode = OSD_OP_WRITE; op->opcode = OSD_OP_WRITE;
op->inode = 0x1000000000001; op->inode = 0x1000000000001;
@@ -65,10 +65,11 @@ int *test_write(cluster_client_t *cli, uint64_t offset, uint64_t len, uint8_t c,
memset(op->iov.buf[0].iov_base, c, len); memset(op->iov.buf[0].iov_base, c, len);
op->callback = [r, cb](cluster_op_t *op) op->callback = [r, cb](cluster_op_t *op)
{ {
if (*r == -1) if (*r == -1000)
printf("Error: Not allowed to complete yet\n"); printf("Error: Not allowed to complete yet\n");
assert(*r != -1); assert(*r != -1000);
*r = op->retval == op->len ? 1 : 0; assert(op->retval == op->len || op->retval < 0);
*r = op->retval == op->len ? 1 : op->retval;
free(op->iov.buf[0].iov_base); free(op->iov.buf[0].iov_base);
printf("Done write %jx+%jx r=%d\n", op->offset, op->len, op->retval); printf("Done write %jx+%jx r=%d\n", op->offset, op->len, op->retval);
delete op; delete op;
@@ -90,14 +91,14 @@ int *test_sync(cluster_client_t *cli)
{ {
printf("Post sync\n"); printf("Post sync\n");
int *r = new int; int *r = new int;
*r = -1; *r = -1000;
cluster_op_t *op = new cluster_op_t(); cluster_op_t *op = new cluster_op_t();
op->opcode = OSD_OP_SYNC; op->opcode = OSD_OP_SYNC;
op->callback = [r](cluster_op_t *op) op->callback = [r](cluster_op_t *op)
{ {
if (*r == -1) if (*r == -1000)
printf("Error: Not allowed to complete yet\n"); printf("Error: Not allowed to complete yet\n");
assert(*r != -1); assert(*r != -1000);
*r = op->retval == 0 ? 1 : 0; *r = op->retval == 0 ? 1 : 0;
printf("Done sync r=%d\n", op->retval); printf("Done sync r=%d\n", op->retval);
delete op; delete op;
@@ -110,7 +111,7 @@ void can_complete(int *r)
{ {
// Allow the operation to proceed so the test verifies // Allow the operation to proceed so the test verifies
// that it doesn't complete earlier than expected // that it doesn't complete earlier than expected
*r = -2; *r = -1001;
} }
void check_completed(int *r) void check_completed(int *r)
@@ -723,6 +724,67 @@ void test_msgr_decrypt_chain()
} }
#endif #endif
void test_vault()
{
json11::Json::object config;
config["vault_url"] = "http://vault";
timerfd_manager_t *tfd = new timerfd_manager_t([](int fd, bool wr, std::function<void(int, int)> callback){});
cluster_client_t *cli = new cluster_client_t(NULL, tfd, config);
configure_single_pg_pool(cli);
pretend_connected(cli, 1);
cli->st_cli.parse_state((etcd_kv_t){
.key = "/config/inode/1/1",
.value = json11::Json::object {
{ "name", "testimg" },
{ "size", (uint64_t)10*1024*1024*1024 },
{ "enc_key", "vault:key1" },
},
});
// No key -> fetch successfully -> complete op
int *r1 = test_write(cli, 0, 4096, 0x55);
check_op_count(cli, 1, 0);
assert(cli->vault_key_load_queue == std::vector<std::string>{"vault:key1"});
cli->vault_key_load_queue.clear();
cli->vault_parse_secret("vault:key1", "", json11::Json::object{
{"data", json11::Json::object {
{"key", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"}
}}
});
can_complete(r1);
check_op_count(cli, 1, 1);
pretend_op_completed(cli, find_op(cli, 1, OSD_OP_WRITE, 0, 4096), 0);
check_completed(r1);
// No key -> error -> EPERM
cli->vault_keys.clear();
cli->inode_cache.clear();
cli->inode_cache_children.clear();
r1 = test_write(cli, 0, 4096, 0x55);
check_op_count(cli, 1, 0);
assert(cli->vault_key_load_queue == std::vector<std::string>{"vault:key1"});
cli->vault_key_load_queue.clear();
can_complete(r1);
cli->vault_parse_secret("vault:key1", "HTTP 403 Forbidden", json11::Json());
check_op_count(cli, 1, 0);
assert(*r1 == -EPERM);
delete r1;
// Free client
delete cli;
delete tfd;
printf("[ok] basic vault key fetch test\n");
}
int main(int narg, char *args[]) int main(int narg, char *args[])
{ {
test1(); test1();
@@ -733,5 +795,6 @@ int main(int narg, char *args[])
test_msgr_encrypt(); test_msgr_encrypt();
test_msgr_decrypt_chain(); test_msgr_decrypt_chain();
#endif #endif
test_vault();
return 0; return 0;
} }