This commit adds the encryption format support for cloned images via the RBD cli,
making the child image be encrypted with a key different from it parent,
while keeping the child thinly-provisioned.
Additionally, other APIs are extended to support flattening of such images.
Signed-off-by: Or Ozeri <oro@il.ibm.com>
:command:`encryption format` *image-spec* *format* *passphrase-file* [--cipher-alg *alg*]
Formats image to an encrypted format.
All data previously written to the image will become unreadable.
- A cloned image cannot be formatted, although encrypted images can be cloned.
Supported formats: *luks1*, *luks2*.
Supported cipher algorithms: *aes-128*, *aes-256* (default).
Enable the specified feature on the specified image. Multiple features can
be specified.
-:command:`flatten` *image-spec*
+:command:`flatten` [--encryption-format *encryption-format* --encryption-passphrase-file *passphrase-file*]... *image-spec*
If image is a clone, copy all shared blocks from the parent snapshot and
make the child independent of the parent, severing the link between
parent snap and child. The parent snapshot can be unprotected and
details.
.. note::
- Currently only flat images (i.e. not cloned) can be formatted.
- Clones of an encrypted image are inherently encrypted using the same format
- and secret.
+ Unless explicitly (re-)formatted, clones of an encrypted image are
+ inherently encrypted using the same format and secret.
+
+.. note::
+ Clones of an encrypted image are always encrypted.
+ Re-formatting to plaintext is not supported.
.. note::
Any data written to the image prior to its format may become unreadable,
set -ex
CEPH_ID=${CEPH_ID:-admin}
-TMP_FILES="/tmp/passphrase /tmp/testdata1 /tmp/testdata2 /tmp/cmpdata"
+TMP_FILES="/tmp/passphrase /tmp/passphrase2 /tmp/testdata1 /tmp/testdata2 /tmp/cmpdata"
_sudo()
{
echo 3 | sudo tee /proc/sys/vm/drop_caches
}
+function expect_false() {
+ if "$@"; then return 1; else return 0; fi
+}
+
function test_encryption_format() {
local format=$1
clean_up_cryptsetup
dd if=/tmp/testdata2 of=/dev/mapper/cryptsetupdev oflag=direct bs=1M
dd if=$LIBRBD_DEV of=/tmp/cmpdata iflag=direct bs=4M count=4
cmp -n 16MB /tmp/cmpdata /tmp/testdata2
+
+ sudo rbd device unmap -t nbd $LIBRBD_DEV
+}
+
+function test_clone_encryption() {
+ clean_up_cryptsetup
+
+ # write 1MB plaintext
+ dd if=/tmp/testdata1 of=$RAW_DEV oflag=direct bs=1M count=1
+
+ # clone (luks1)
+ rbd snap create testimg@snap
+ rbd snap protect testimg@snap
+ rbd clone testimg@snap testimg1
+ rbd encryption format testimg1 luks1 /tmp/passphrase
+
+ # open encryption with librbd, write one more MB, close
+ LIBRBD_DEV=$(_sudo rbd -p rbd map testimg1 -t nbd -o encryption-format=luks1,encryption-passphrase-file=/tmp/passphrase)
+ sudo chmod 666 $LIBRBD_DEV
+ dd if=$LIBRBD_DEV of=/tmp/cmpdata iflag=direct bs=1M count=1
+ cmp -n 1MB /tmp/cmpdata /tmp/testdata1
+ dd if=/tmp/testdata1 of=$LIBRBD_DEV seek=1 skip=1 oflag=direct bs=1M count=1
+ sudo rbd device unmap -t nbd $LIBRBD_DEV
+
+ # second clone (luks2)
+ rbd snap create testimg1@snap
+ rbd snap protect testimg1@snap
+ rbd clone testimg1@snap testimg2
+ rbd encryption format testimg2 luks2 /tmp/passphrase2
+
+ # open encryption with librbd, write one more MB, close
+ LIBRBD_DEV=$(_sudo rbd -p rbd map testimg2 -t nbd -o encryption-format=luks2,encryption-passphrase-file=/tmp/passphrase2,encryption-format=luks1,encryption-passphrase-file=/tmp/passphrase)
+ sudo chmod 666 $LIBRBD_DEV
+ dd if=$LIBRBD_DEV of=/tmp/cmpdata iflag=direct bs=1M count=2
+ cmp -n 2MB /tmp/cmpdata /tmp/testdata1
+ dd if=/tmp/testdata1 of=$LIBRBD_DEV seek=2 skip=2 oflag=direct bs=1M count=1
+ sudo rbd device unmap -t nbd $LIBRBD_DEV
+
+ # flatten
+ rbd flatten testimg2 --encryption-format luks --encryption-format luks --encryption-passphrase-file /tmp/passphrase2 --encryption-passphrase-file /tmp/passphrase
+
+ # verify with cryptsetup
+ RAW_FLAT_DEV=$(_sudo rbd -p rbd map testimg2 -t nbd)
+ sudo cryptsetup open $RAW_FLAT_DEV --type luks cryptsetupdev -d /tmp/passphrase2
+ sudo chmod 666 /dev/mapper/cryptsetupdev
+ dd if=/dev/mapper/cryptsetupdev of=/tmp/cmpdata iflag=direct bs=1M count=3
+ cmp -n 3MB /tmp/cmpdata /tmp/testdata1
+ sudo rbd device unmap -t nbd $RAW_FLAT_DEV
+}
+
+function test_clone_and_load_with_a_single_passphrase {
+ local expectedfail=$1
+
+ # clone and format
+ rbd snap create testimg@snap
+ rbd snap protect testimg@snap
+ rbd clone testimg@snap testimg1
+ rbd encryption format testimg1 luks2 /tmp/passphrase2
+
+ if [ "$expectedfail" = "true" ]
+ then
+ expect_false rbd flatten testimg1 --encryption-format luks --encryption-passphrase-file /tmp/passphrase2
+ else
+ rbd flatten testimg1 --encryption-format luks --encryption-passphrase-file /tmp/passphrase2
+ fi
+
+ rbd remove testimg1
+ rbd snap unprotect testimg@snap
+ rbd snap remove testimg@snap
+}
+
+function test_plaintext_detection {
+ # 16k LUKS header
+ sudo cryptsetup -q luksFormat --type luks2 --luks2-metadata-size 16k $RAW_DEV /tmp/passphrase
+ test_clone_and_load_with_a_single_passphrase true
+
+ # 4m LUKS header
+ sudo cryptsetup -q luksFormat --type luks2 --luks2-metadata-size 4m $RAW_DEV /tmp/passphrase
+ test_clone_and_load_with_a_single_passphrase true
+
+ # no luks header
+ dd if=/dev/zero of=$RAW_DEV oflag=direct bs=4M count=8
+ test_clone_and_load_with_a_single_passphrase false
}
function get_nbd_device_paths {
- rbd device list -t nbd | tail -n +2 | egrep "\s+rbd\s+testimg\s+" | awk '{print $5;}'
+ rbd device list -t nbd | tail -n +2 | egrep "\s+rbd\s+testimg" | awk '{print $5;}'
}
function clean_up_cryptsetup() {
function clean_up {
sudo rm -f $TMP_FILES
clean_up_cryptsetup
- for device in $(get_nbd_device_paths); do
- _sudo rbd device unmap -t nbd $device
+ for device in $(get_nbd_device_paths); do
+ _sudo rbd device unmap -t nbd $device
done
- rbd ls | grep testimg > /dev/null && rbd rm testimg || true
+
+ rbd remove testimg2 || true
+ rbd snap unprotect testimg1@snap || true
+ rbd snap remove testimg1@snap || true
+ rbd remove testimg1 || true
+ rbd snap unprotect testimg@snap || true
+ rbd snap remove testimg@snap || true
+ rbd remove testimg || true
}
if [[ $(uname) != "Linux" ]]; then
dd if=/dev/urandom of=/tmp/testdata1 bs=4M count=4
dd if=/dev/urandom of=/tmp/testdata2 bs=4M count=4
-# create passphrase file
+# create passphrase files
echo -n "password" > /tmp/passphrase
+echo -n "password2" > /tmp/passphrase2
# create an image
rbd create testimg --size=32M
# map raw data to nbd device
RAW_DEV=$(_sudo rbd -p rbd map testimg -t nbd)
+sudo chmod 666 $RAW_DEV
+
+test_plaintext_detection
test_encryption_format luks1
test_encryption_format luks2
+test_clone_encryption
+
echo OK
rbd help flatten
usage: rbd flatten [--pool <pool>] [--namespace <namespace>] [--image <image>]
- [--no-progress]
+ [--no-progress] [--encryption-format <encryption-format>]
+ [--encryption-passphrase-file <encryption-passphrase-file>]
<image-spec>
Fill clone with parent data (make it independent).
Positional arguments
- <image-spec> image specification
- (example: [<pool-name>/[<namespace>/]]<image-name>)
+ <image-spec> image specification
+ (example:
+ [<pool-name>/[<namespace>/]]<image-name>)
Optional arguments
- -p [ --pool ] arg pool name
- --namespace arg namespace name
- --image arg image name
- --no-progress disable progress output
+ -p [ --pool ] arg pool name
+ --namespace arg namespace name
+ --image arg image name
+ --no-progress disable progress output
+ --encryption-format arg encryption formats [possible values: luks]
+ --encryption-passphrase-file arg path to file containing passphrase for
+ unlocking the image
rbd help group create
usage: rbd group create [--pool <pool>] [--namespace <namespace>]
--namespace arg namespace name
--image arg image name
+
"ignore quiesce hook error");
}
+void add_encryption_options(boost::program_options::options_description *opt) {
+ opt->add_options()
+ (ENCRYPTION_FORMAT.c_str(),
+ po::value<std::vector<EncryptionFormat>>(),
+ "encryption formats [possible values: luks]");
+
+ opt->add_options()
+ (ENCRYPTION_PASSPHRASE_FILE.c_str(),
+ po::value<std::vector<std::string>>(),
+ "path to file containing passphrase for unlocking the image");
+}
+
std::string get_short_features_help(bool append_suffix) {
std::ostringstream oss;
bool first_feature = true;
}
}
+void validate(boost::any& v, const std::vector<std::string>& values,
+ EncryptionFormat *target_type, int) {
+ po::validators::check_first_occurrence(v);
+ const std::string &s = po::validators::get_single_string(values);
+ EncryptionFormat format;
+ if (s == "luks") {
+ format.format = RBD_ENCRYPTION_FORMAT_LUKS;
+ } else {
+ throw po::validation_error(po::validation_error::invalid_option_value);
+ }
+
+ v = boost::any(format);
+}
+
void validate(boost::any& v, const std::vector<std::string>& values,
ExportFormat *target_type, int) {
po::validators::check_first_occurrence(v);
static const std::string FROM_SNAPSHOT_NAME("from-snap");
static const std::string WHOLE_OBJECT("whole-object");
+// encryption arguments
+static const std::string ENCRYPTION_FORMAT("encryption-format");
+static const std::string ENCRYPTION_PASSPHRASE_FILE("encryption-passphrase-file");
+
static const std::string IMAGE_FORMAT("image-format");
static const std::string IMAGE_NEW_FORMAT("new-format");
static const std::string IMAGE_ORDER("order");
struct Secret {};
struct EncryptionAlgorithm {};
+struct EncryptionFormat {
+ uint64_t format;
+};
void add_export_format_option(boost::program_options::options_description *opt);
void add_snap_create_options(boost::program_options::options_description *opt);
+void add_encryption_options(boost::program_options::options_description *opt);
+
std::string get_short_features_help(bool append_suffix);
std::string get_long_features_help();
JournalObjectSize *target_type, int);
void validate(boost::any& v, const std::vector<std::string>& values,
EncryptionAlgorithm *target_type, int);
+void validate(boost::any& v, const std::vector<std::string>& values,
+ EncryptionFormat *target_type, int);
void validate(boost::any& v, const std::vector<std::string>& values,
Secret *target_type, int);
#include "common/escape.h"
#include "common/safe_io.h"
#include "global/global_context.h"
+#include <fstream>
#include <iostream>
#include <regex>
#include <boost/algorithm/string.hpp>
return 0;
}
+int get_encryption_options(const boost::program_options::variables_map &vm,
+ EncryptionOptions* opts) {
+ std::vector<std::string> passphrase_files;
+ if (vm.count(at::ENCRYPTION_PASSPHRASE_FILE)) {
+ passphrase_files =
+ vm[at::ENCRYPTION_PASSPHRASE_FILE].as<std::vector<std::string>>();
+ }
+
+ std::vector<librbd::encryption_format_t> formats;
+ if (vm.count(at::ENCRYPTION_FORMAT)) {
+ auto& format_structs =
+ vm[at::ENCRYPTION_FORMAT].as<std::vector<at::EncryptionFormat>>();
+ for (auto& format_struct : format_structs) {
+ formats.push_back((librbd::encryption_format_t)format_struct.format);
+ }
+ }
+
+ if (formats.size() != passphrase_files.size()) {
+ std::cerr << "rbd: encryption formats count does not match "
+ << "passphrase files count" << std::endl;
+ return -EINVAL;
+ }
+
+ auto spec_count = formats.size();
+ if (spec_count == 0) {
+ return 0;
+ }
+
+ opts->luks_opts.reserve(spec_count);
+
+ auto& specs = opts->specs;
+ specs.resize(spec_count);
+ for (size_t i = 0; i < spec_count; ++i) {
+ std::ifstream file(passphrase_files[i].c_str());
+ auto sg = make_scope_guard([&] { file.close(); });
+
+ specs[i].format = formats[i];
+ std::string* passphrase;
+ switch (specs[i].format) {
+ case RBD_ENCRYPTION_FORMAT_LUKS: {
+ auto& luks_opts = opts->luks_opts;
+ luks_opts.emplace_back();
+ specs[i].opts = &luks_opts.back();
+ specs[i].opts_size = sizeof(luks_opts.back());
+ passphrase = &luks_opts.back().passphrase;
+ break;
+ }
+ default:
+ std::cerr << "rbd: unsupported encryption format: " << specs[i].format
+ << std::endl;
+ return -ENOTSUP;
+ }
+
+ passphrase->assign((std::istreambuf_iterator<char>(file)),
+ (std::istreambuf_iterator<char>()));
+
+ if (file.fail()) {
+ std::cerr << "rbd: unable to open passphrase file '"
+ << passphrase_files[i] << "': " << cpp_strerror(errno)
+ << std::endl;
+ return -errno;
+ }
+
+ if (!passphrase->empty() &&
+ (*passphrase)[passphrase->length() - 1] == '\n') {
+ passphrase->erase(passphrase->length() - 1);
+ }
+ }
+
+ return 0;
+}
+
void init_context() {
g_conf().set_val_or_die("rbd_cache_writethrough_until_flush", "false");
g_conf().apply_changes(nullptr);
return r;
}
}
+
return 0;
}
#ifndef CEPH_RBD_UTILS_H
#define CEPH_RBD_UTILS_H
+#include "include/compat.h"
#include "include/int_types.h"
#include "include/rados/librados.hpp"
#include "include/rbd/librbd.hpp"
int get_percentage(uint64_t part, uint64_t whole);
+struct EncryptionOptions {
+ std::vector<librbd::encryption_spec_t> specs;
+ std::vector<librbd::encryption_luks_format_options_t> luks_opts;
+
+ ~EncryptionOptions() {
+ for (auto& opts : luks_opts) {
+ auto& passphrase = opts.passphrase;
+ ceph_memzero_s(&passphrase[0], passphrase.size(), passphrase.size());
+ }
+ }
+};
+
template <typename T, void(T::*MF)(int)>
librbd::RBD::AioCompletion *create_aio_completion(T *t) {
return new librbd::RBD::AioCompletion(
int get_snap_create_flags(const boost::program_options::variables_map &vm,
uint32_t *flags);
+int get_encryption_options(const boost::program_options::variables_map &vm,
+ EncryptionOptions* opts);
+
void init_context();
int init_rados(librados::Rados *rados);
po::options_description *options) {
at::add_image_spec_options(positional, options, at::ARGUMENT_MODIFIER_NONE);
at::add_no_progress_option(options);
+ at::add_encryption_options(options);
}
int execute(const po::variables_map &vm,
return r;
}
+ utils::EncryptionOptions encryption_options;
+ r = utils::get_encryption_options(vm, &encryption_options);
+ if (r < 0) {
+ return r;
+ }
+
librados::Rados rados;
librados::IoCtx io_ctx;
librbd::Image image;
return r;
}
+ auto spec_count = encryption_options.specs.size();
+ if (spec_count > 0) {
+ r = image.encryption_load2(&encryption_options.specs[0], spec_count);
+
+ if (r < 0) {
+ std::cerr << "rbd: encryption load failed: " << cpp_strerror(r)
+ << std::endl;
+ return r;
+ }
+ }
+
r = do_flatten(image, vm[at::NO_PROGRESS].as<bool>());
if (r < 0) {
std::cerr << "rbd: flatten error: " << cpp_strerror(r) << std::endl;
std::string format;
bool pretty_format = false;
- std::optional<librbd::encryption_format_t> encryption_format;
- std::optional<std::string> encryption_passphrase_file;
+ std::vector<librbd::encryption_format_t> encryption_format;
+ std::vector<std::string> encryption_passphrase_file;
Command command = None;
int pid = 0;
unsigned long size;
unsigned long blksize = RBD_NBD_BLKSIZE;
bool use_netlink;
+ auto encryption_format_count = cfg->encryption_format.size();
int fd[2];
goto close_fd;
}
- if (cfg->encryption_format.has_value()) {
- if (!cfg->encryption_passphrase_file.has_value()) {
- r = -EINVAL;
- cerr << "rbd-nbd: missing encryption-passphrase-file" << std::endl;
- goto close_fd;
- }
- std::ifstream file(cfg->encryption_passphrase_file.value().c_str());
- if (file.fail()) {
- r = -errno;
- std::cerr << "rbd-nbd: unable to open passphrase file:"
- << cpp_strerror(errno) << std::endl;
- goto close_fd;
- }
- std::string passphrase((std::istreambuf_iterator<char>(file)),
- (std::istreambuf_iterator<char>()));
- auto sg = make_scope_guard([&] {
- ceph_memzero_s(&passphrase[0], passphrase.size(), passphrase.size()); });
- file.close();
- if (!passphrase.empty() && passphrase[passphrase.length() - 1] == '\n') {
- passphrase.erase(passphrase.length() - 1);
- }
+ if (encryption_format_count > 0) {
+ std::vector<librbd::encryption_spec_t> specs(encryption_format_count);
+ std::vector<librbd::encryption_luks_format_options_t> luks_opts;
- switch (cfg->encryption_format.value()) {
- case RBD_ENCRYPTION_FORMAT_LUKS1: {
- librbd::encryption_luks1_format_options_t opts = {};
- opts.passphrase = passphrase;
- r = image.encryption_load(
- RBD_ENCRYPTION_FORMAT_LUKS1, &opts, sizeof(opts));
- blksize = 4096;
- break;
- }
- case RBD_ENCRYPTION_FORMAT_LUKS2: {
- librbd::encryption_luks2_format_options_t opts = {};
- opts.passphrase = passphrase;
- r = image.encryption_load(
- RBD_ENCRYPTION_FORMAT_LUKS2, &opts, sizeof(opts));
- blksize = 4096;
- break;
+ luks_opts.reserve(encryption_format_count);
+
+ auto sg = make_scope_guard([&] {
+ for (auto& opts : luks_opts) {
+ auto& passphrase = opts.passphrase;
+ ceph_memzero_s(&passphrase[0], passphrase.size(), passphrase.size());
}
- case RBD_ENCRYPTION_FORMAT_LUKS: {
- librbd::encryption_luks_format_options_t opts = {};
- opts.passphrase = passphrase;
- r = image.encryption_load(
- RBD_ENCRYPTION_FORMAT_LUKS, &opts, sizeof(opts));
- blksize = 4096;
- break;
+ });
+
+ for (size_t i = 0; i < encryption_format_count; ++i) {
+ std::ifstream file(cfg->encryption_passphrase_file[i].c_str());
+ auto sg2 = make_scope_guard([&] { file.close(); });
+
+ specs[i].format = cfg->encryption_format[i];
+ std::string* passphrase;
+ switch (specs[i].format) {
+ case RBD_ENCRYPTION_FORMAT_LUKS: {
+ luks_opts.emplace_back();
+ specs[i].opts = &luks_opts.back();
+ specs[i].opts_size = sizeof(luks_opts.back());
+ passphrase = &luks_opts.back().passphrase;
+ break;
+ }
+ default:
+ r = -ENOTSUP;
+ cerr << "rbd-nbd: unsupported encryption format: " << specs[i].format
+ << std::endl;
+ goto close_fd;
}
- default:
- r = -ENOTSUP;
- cerr << "rbd-nbd: unsupported encryption format" << std::endl;
+
+ passphrase->assign((std::istreambuf_iterator<char>(file)),
+ (std::istreambuf_iterator<char>()));
+
+ if (file.fail()) {
+ r = -errno;
+ std::cerr << "rbd-nbd: unable to open passphrase file '"
+ << cfg->encryption_passphrase_file[i] << "': "
+ << cpp_strerror(errno) << std::endl;
goto close_fd;
+ }
+
+ if (!passphrase->empty() &&
+ (*passphrase)[passphrase->length() - 1] == '\n') {
+ passphrase->erase(passphrase->length() - 1);
+ }
}
+ r = image.encryption_load2(&specs[0], encryption_format_count);
+
if (r != 0) {
cerr << "rbd-nbd: failed to load encryption: " << cpp_strerror(r)
<< std::endl;
goto close_fd;
}
+
+ // luks2 block size can vary upto 4096, while luks1 always uses 512
+ // currently we don't have an rbd API for querying the loaded encryption
+ blksize = 4096;
}
r = image.stat(info, sizeof(info));
} else if (ceph_argparse_witharg(args, i, &arg_value,
"--encryption-format", (char *)NULL)) {
if (arg_value == "luks1") {
- cfg->encryption_format =
- std::make_optional(RBD_ENCRYPTION_FORMAT_LUKS);
+ cfg->encryption_format.push_back(RBD_ENCRYPTION_FORMAT_LUKS);
*err_msg << "rbd-nbd: specifying luks1 when loading encryption "
"is deprecated, use luks";
} else if (arg_value == "luks2") {
- cfg->encryption_format =
- std::make_optional(RBD_ENCRYPTION_FORMAT_LUKS);
+ cfg->encryption_format.push_back(RBD_ENCRYPTION_FORMAT_LUKS);
*err_msg << "rbd-nbd: specifying luks2 when loading encryption "
"is deprecated, use luks";
} else if (arg_value == "luks") {
- cfg->encryption_format =
- std::make_optional(RBD_ENCRYPTION_FORMAT_LUKS);
+ cfg->encryption_format.push_back(RBD_ENCRYPTION_FORMAT_LUKS);
} else {
*err_msg << "rbd-nbd: Invalid encryption format";
return -EINVAL;
} else if (ceph_argparse_witharg(args, i, &arg_value,
"--encryption-passphrase-file",
(char *)NULL)) {
- cfg->encryption_passphrase_file = std::make_optional(arg_value);
+ cfg->encryption_passphrase_file.push_back(arg_value);
} else {
++i;
}
}
+ if (cfg->encryption_format.size() != cfg->encryption_passphrase_file.size()) {
+ *err_msg << "rbd-nbd: Encryption formats count does not match "
+ << "passphrase files count";
+ return -EINVAL;
+ }
+
Command cmd = None;
if (args.begin() != args.end()) {
if (strcmp(*args.begin(), "map") == 0) {