Skip to content

Commit

Permalink
Merge pull request #50986 from arenadata/ADQM-822
Browse files Browse the repository at this point in the history
Implement support of encrypted elements in configuration file
  • Loading branch information
rschu1ze committed Jul 26, 2023
2 parents 4d03c23 + 0af869f commit d4737ca
Show file tree
Hide file tree
Showing 21 changed files with 423 additions and 13 deletions.
34 changes: 34 additions & 0 deletions docs/en/operations/configuration-files.md
Expand Up @@ -65,6 +65,40 @@ XML substitution example:

Substitutions can also be performed from ZooKeeper. To do this, specify the attribute `from_zk = "/path/to/node"`. The element value is replaced with the contents of the node at `/path/to/node` in ZooKeeper. You can also put an entire XML subtree on the ZooKeeper node and it will be fully inserted into the source element.

## Encrypting Configuration {#encryption}

You can use symmetric encryption to encrypt a configuration element, for example, a password field. To do so, first configure the [encryption codec](../sql-reference/statements/create/table.md#encryption-codecs), then add attribute `encryption_codec` with the name of the encryption codec as value to the element to encrypt.

Unlike attributes `from_zk`, `from_env` and `incl` (or element `include`), no substitution, i.e. decryption of the encrypted value, is performed in the preprocessed file. Decryption happens only at runtime in the server process.

Example:

```xml
<clickhouse>
<encryption_codecs>
<aes_128_gcm_siv>
<key_hex>00112233445566778899aabbccddeeff</key_hex>
</aes_128_gcm_siv>
</encryption_codecs>
<interserver_http_credentials>
<user>admin</user>
<password encryption_codec="AES_128_GCM_SIV">961F000000040000000000EEDDEF4F453CFE6457C4234BD7C09258BD651D85</password>
</interserver_http_credentials>
</clickhouse>
```

To get the encrypted value `encrypt_decrypt` example application may be used.

Example:

``` bash
./encrypt_decrypt /etc/clickhouse-server/config.xml -e AES_128_GCM_SIV abcd
```

``` text
961F000000040000000000EEDDEF4F453CFE6457C4234BD7C09258BD651D85
```

## User Settings {#user-settings}

The `config.xml` file can specify a separate config with user settings, profiles, and quotas. The relative path to this config is set in the `users_config` element. By default, it is `users.xml`. If `users_config` is omitted, the user settings, profiles, and quotas are specified directly in `config.xml`.
Expand Down
34 changes: 34 additions & 0 deletions docs/ru/operations/configuration-files.md
Expand Up @@ -85,6 +85,40 @@ $ cat /etc/clickhouse-server/users.d/alice.xml

Сервер следит за изменениями конфигурационных файлов, а также файлов и ZooKeeper-узлов, которые были использованы при выполнении подстановок и переопределений, и перезагружает настройки пользователей и кластеров на лету. То есть, можно изменять кластера, пользователей и их настройки без перезапуска сервера.

## Шифрование {#encryption}

Вы можете использовать симметричное шифрование для зашифровки элемента конфигурации, например, поля password. Чтобы это сделать, сначала настройте [кодек шифрования](../sql-reference/statements/create/table.md#encryption-codecs), затем добавьте аттибут`encryption_codec` с именем кодека шифрования как значение к элементу, который надо зашифровать.

В отличии от аттрибутов `from_zk`, `from_env` и `incl` (или элемента `include`), подстановка, т.е. расшифровка зашифрованного значения, не выподняется в файле предобработки. Расшифровка происходит только во время исполнения в серверном процессе.

Пример:

```xml
<clickhouse>
<encryption_codecs>
<aes_128_gcm_siv>
<key_hex>00112233445566778899aabbccddeeff</key_hex>
</aes_128_gcm_siv>
</encryption_codecs>
<interserver_http_credentials>
<user>admin</user>
<password encryption_codec="AES_128_GCM_SIV">961F000000040000000000EEDDEF4F453CFE6457C4234BD7C09258BD651D85</password>
</interserver_http_credentials>
</clickhouse>
```

Чтобы получить зашифрованное значение может быть использовано приложение-пример `encrypt_decrypt` .

Пример:

``` bash
./encrypt_decrypt /etc/clickhouse-server/config.xml -e AES_128_GCM_SIV abcd
```

``` text
961F000000040000000000EEDDEF4F453CFE6457C4234BD7C09258BD651D85
```

## Примеры записи конфигурации на YAML {#example}

Здесь можно рассмотреть пример реальной конфигурации записанной на YAML: [config.yaml.example](https://github.com/ClickHouse/ClickHouse/blob/master/programs/server/config.yaml.example).
Expand Down
1 change: 1 addition & 0 deletions programs/keeper/CMakeLists.txt
Expand Up @@ -80,6 +80,7 @@ if (BUILD_STANDALONE_KEEPER)
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Compression/CompressedReadBuffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Compression/CompressedReadBufferFromFile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Compression/CompressedWriteBuffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Compression/CompressionCodecEncrypted.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Compression/CompressionCodecLZ4.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Compression/CompressionCodecMultiple.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Compression/CompressionCodecNone.cpp
Expand Down
97 changes: 96 additions & 1 deletion src/Common/Config/ConfigProcessor.cpp
Expand Up @@ -26,6 +26,14 @@
#include <IO/WriteBufferFromString.h>
#include <IO/Operators.h>

#if USE_SSL
#include <format>
#include <IO/BufferWithOwnMemory.h>
#include <Compression/ICompressionCodec.h>
#include <Compression/CompressionCodecEncrypted.h>
#include <boost/algorithm/hex.hpp>
#endif

#define PREPROCESSED_SUFFIX "-preprocessed"

namespace fs = std::filesystem;
Expand All @@ -39,6 +47,9 @@ namespace ErrorCodes
{
extern const int FILE_DOESNT_EXIST;
extern const int CANNOT_LOAD_CONFIG;
#if USE_SSL
extern const int BAD_ARGUMENTS;
#endif
}

/// For cutting preprocessed path to this base
Expand Down Expand Up @@ -177,6 +188,72 @@ static void mergeAttributes(Element & config_element, Element & with_element)
with_element_attributes->release();
}

#if USE_SSL

std::string ConfigProcessor::encryptValue(const std::string & codec_name, const std::string & value)
{
EncryptionMethod method = getEncryptionMethod(codec_name);
CompressionCodecEncrypted codec(method);

Memory<> memory;
memory.resize(codec.getCompressedReserveSize(static_cast<UInt32>(value.size())));
auto bytes_written = codec.compress(value.data(), static_cast<UInt32>(value.size()), memory.data());
auto encrypted_value = std::string(memory.data(), bytes_written);
std::string hex_value;
boost::algorithm::hex(encrypted_value.begin(), encrypted_value.end(), std::back_inserter(hex_value));
return hex_value;
}

std::string ConfigProcessor::decryptValue(const std::string & codec_name, const std::string & value)
{
EncryptionMethod method = getEncryptionMethod(codec_name);
CompressionCodecEncrypted codec(method);

Memory<> memory;
std::string encrypted_value;

try
{
boost::algorithm::unhex(value, std::back_inserter(encrypted_value));
}
catch (const std::exception &)
{
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Cannot read encrypted text, check for valid characters [0-9a-fA-F] and length");
}

memory.resize(codec.readDecompressedBlockSize(encrypted_value.data()));
codec.decompress(encrypted_value.data(), static_cast<UInt32>(encrypted_value.size()), memory.data());
std::string decrypted_value = std::string(memory.data(), memory.size());
return decrypted_value;
}

void ConfigProcessor::decryptRecursive(Poco::XML::Node * config_root)
{
for (Node * node = config_root->firstChild(); node; node = node->nextSibling())
{
if (node->nodeType() == Node::ELEMENT_NODE)
{
Element & element = dynamic_cast<Element &>(*node);
if (element.hasAttribute("encryption_codec"))
{
const NodeListPtr children = element.childNodes();
if (children->length() != 1)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Encrypted node {} cannot contain nested elements", node->nodeName());

Node * text_node = node->firstChild();
if (text_node->nodeType() != Node::TEXT_NODE)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Encrypted node {} should have text node", node->nodeName());

auto encryption_codec = element.getAttribute("encryption_codec");
text_node->setNodeValue(decryptValue(encryption_codec, text_node->getNodeValue()));
}
decryptRecursive(node);
}
}
}

#endif

void ConfigProcessor::mergeRecursive(XMLDocumentPtr config, Node * config_root, const Node * with_root)
{
const NodeListPtr with_nodes = with_root->childNodes();
Expand Down Expand Up @@ -694,7 +771,19 @@ ConfigProcessor::LoadedConfig ConfigProcessor::loadConfigWithZooKeeperIncludes(
return LoadedConfig{configuration, has_zk_includes, !processed_successfully, config_xml, path};
}

void ConfigProcessor::savePreprocessedConfig(const LoadedConfig & loaded_config, std::string preprocessed_dir)
#if USE_SSL

void ConfigProcessor::decryptEncryptedElements(LoadedConfig & loaded_config)
{
CompressionCodecEncrypted::Configuration::instance().tryLoad(*loaded_config.configuration, "encryption_codecs");
Node * config_root = getRootNode(loaded_config.preprocessed_xml.get());
decryptRecursive(config_root);
loaded_config.configuration = new Poco::Util::XMLConfiguration(loaded_config.preprocessed_xml);
}

#endif

void ConfigProcessor::savePreprocessedConfig(LoadedConfig & loaded_config, std::string preprocessed_dir)
{
try
{
Expand Down Expand Up @@ -749,6 +838,12 @@ void ConfigProcessor::savePreprocessedConfig(const LoadedConfig & loaded_config,
{
LOG_WARNING(log, "Couldn't save preprocessed config to {}: {}", preprocessed_path, e.displayText());
}

#if USE_SSL
std::string preprocessed_file_name = fs::path(preprocessed_path).filename();
if (preprocessed_file_name == "config.xml" || preprocessed_file_name == std::format("config{}.xml", PREPROCESSED_SUFFIX))
decryptEncryptedElements(loaded_config);
#endif
}

void ConfigProcessor::setConfigPath(const std::string & config_path)
Expand Down
17 changes: 16 additions & 1 deletion src/Common/Config/ConfigProcessor.h
Expand Up @@ -97,7 +97,7 @@ class ConfigProcessor

/// Save preprocessed config to specified directory.
/// If preprocessed_dir is empty - calculate from loaded_config.path + /preprocessed_configs/
void savePreprocessedConfig(const LoadedConfig & loaded_config, std::string preprocessed_dir);
void savePreprocessedConfig(LoadedConfig & loaded_config, std::string preprocessed_dir);

/// Set path of main config.xml. It will be cut from all configs placed to preprocessed_configs/
static void setConfigPath(const std::string & config_path);
Expand All @@ -109,6 +109,14 @@ class ConfigProcessor
/// Is the file named as result of config preprocessing, not as original files.
static bool isPreprocessedFile(const std::string & config_path);

#if USE_SSL
/// Encrypt text value
static std::string encryptValue(const std::string & codec_name, const std::string & value);

/// Decrypt value
static std::string decryptValue(const std::string & codec_name, const std::string & value);
#endif

static inline const auto SUBSTITUTION_ATTRS = {"incl", "from_zk", "from_env"};

private:
Expand All @@ -127,6 +135,13 @@ class ConfigProcessor

using NodePtr = Poco::AutoPtr<Poco::XML::Node>;

#if USE_SSL
void decryptRecursive(Poco::XML::Node * config_root);

/// Decrypt elements in config with specified encryption attributes
void decryptEncryptedElements(LoadedConfig & loaded_config);
#endif

void mergeRecursive(XMLDocumentPtr config, Poco::XML::Node * config_root, const Poco::XML::Node * with_root);

void merge(XMLDocumentPtr config, XMLDocumentPtr with);
Expand Down
20 changes: 17 additions & 3 deletions src/Common/Config/YAMLParser.cpp
Expand Up @@ -110,9 +110,23 @@ namespace
}
else
{
Poco::AutoPtr<Poco::XML::Element> xml_key = xml_document->createElement(key);
parent_xml_node.appendChild(xml_key);
processNode(value_node, *xml_key);
if (key == "#text" && value_node.IsScalar())
{
for (Node * child_node = parent_xml_node.firstChild(); child_node; child_node = child_node->nextSibling())
if (child_node->nodeType() == Node::TEXT_NODE)
throw Exception(ErrorCodes::CANNOT_PARSE_YAML,
"YAMLParser has encountered node with several text nodes "
"and cannot continue parsing of the file");
std::string value = value_node.as<std::string>();
Poco::AutoPtr<Poco::XML::Text> xml_value = xml_document->createTextNode(value);
parent_xml_node.appendChild(xml_value);
}
else
{
Poco::AutoPtr<Poco::XML::Element> xml_key = xml_document->createElement(key);
parent_xml_node.appendChild(xml_key);
processNode(value_node, *xml_key);
}
}
}
break;
Expand Down
5 changes: 5 additions & 0 deletions src/Common/examples/CMakeLists.txt
Expand Up @@ -82,3 +82,8 @@ endif()

clickhouse_add_executable (interval_tree interval_tree.cpp)
target_link_libraries (interval_tree PRIVATE dbms)

if (ENABLE_SSL)
clickhouse_add_executable (encrypt_decrypt encrypt_decrypt.cpp)
target_link_libraries (encrypt_decrypt PRIVATE dbms)
endif()
61 changes: 61 additions & 0 deletions src/Common/examples/encrypt_decrypt.cpp
@@ -0,0 +1,61 @@
#include <Common/Config/ConfigProcessor.h>
#include <Compression/ICompressionCodec.h>
#include <Compression/CompressionCodecEncrypted.h>
#include <iostream>

/** This test program encrypts or decrypts text values using a symmetric encryption codec like AES_128_GCM_SIV or AES_256_GCM_SIV.
* Keys for codecs are loaded from <encryption_codecs> section of configuration file.
*
* How to use:
* ./encrypt_decrypt /etc/clickhouse-server/config.xml -e AES_128_GCM_SIV text_to_encrypt
*/

int main(int argc, char ** argv)
{
try
{
if (argc != 5)
{
std::cerr << "Usage:" << std::endl
<< " " << argv[0] << " path action codec value" << std::endl
<< "path: path to configuration file." << std::endl
<< "action: -e for encryption and -d for decryption." << std::endl
<< "codec: AES_128_GCM_SIV or AES_256_GCM_SIV." << std::endl << std::endl
<< "Example:" << std::endl
<< " ./encrypt_decrypt /etc/clickhouse-server/config.xml -e AES_128_GCM_SIV text_to_encrypt";
return 3;
}

std::string action = argv[2];
std::string codec_name = argv[3];
std::string value = argv[4];

DB::ConfigProcessor processor(argv[1], false, true);
auto loaded_config = processor.loadConfig();
DB::CompressionCodecEncrypted::Configuration::instance().tryLoad(*loaded_config.configuration, "encryption_codecs");

if (action == "-e")
std::cout << processor.encryptValue(codec_name, value) << std::endl;
else if (action == "-d")
std::cout << processor.decryptValue(codec_name, value) << std::endl;
else
std::cerr << "Unknown action: " << action << std::endl;
}
catch (Poco::Exception & e)
{
std::cerr << "Exception: " << e.displayText() << std::endl;
return 1;
}
catch (std::exception & e)
{
std::cerr << "std::exception: " << e.what() << std::endl;
return 3;
}
catch (...)
{
std::cerr << "Some exception" << std::endl;
return 2;
}

return 0;
}

0 comments on commit d4737ca

Please sign in to comment.