Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support of encrypted elements in configuration file #50986

Merged
merged 40 commits into from Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
616904c
Add encryptConfig()
rvasin May 23, 2023
dd78008
Rename encryptConfig() into decryptConfig()
rvasin May 25, 2023
5f73681
Make working note descryption
rvasin May 25, 2023
cd8eb44
Add encryptValue(), decryptValue() and exceptions
rvasin May 31, 2023
fd8c599
Add encrypt_decrypt example
rvasin May 31, 2023
0708cae
Fix style
rvasin Jun 1, 2023
2ccec01
Set correct memory size for encrypt/decrypt
rvasin Jun 1, 2023
d5add61
Add text memo for encrypt_decrypt
rvasin Jun 1, 2023
e269235
Make decryptRecursive() go through element nodes only
rvasin Jun 1, 2023
d316add
Add integration test test_config_decryption
rvasin Jun 6, 2023
1bce32c
Add tests for wrong settings
rvasin Jun 9, 2023
a4e9824
Update documentation
rvasin Jun 14, 2023
b5d4ad5
Small code style improvements
rvasin Jun 14, 2023
f55623a
Use anonymous namespace for getEncryptionMethod()
rvasin Jun 14, 2023
14dfebb
Fix links in MD
rvasin Jun 14, 2023
3d64cf4
Add dbms in cmake
rvasin Jun 14, 2023
f830f24
Revert "Add dbms in cmake"
rvasin Jun 15, 2023
98597a3
Add USE_SSL
rvasin Jun 15, 2023
5501334
Fix code align in cmake
rvasin Jun 16, 2023
f026cf1
Fix building with BUILD_STANDALONE_KEEPER
rvasin Jun 16, 2023
d55878d
Merge branch 'master' into ADQM-822
rvasin Jun 16, 2023
5bba0ff
Fix build of keeper-bench
rvasin Jun 16, 2023
ce13131
Fix integration tests
rvasin Jul 11, 2023
b6023d9
Merge branch 'master' of github.com:ClickHouse/ClickHouse into ADQM-822
rvasin Jul 11, 2023
a73dca1
Move getEncryptionMethod to CompressionCodecEncrypted.h
rvasin Jul 11, 2023
3b8ecb1
Move descryption code to savePreprocessedConfig
rvasin Jul 11, 2023
b9adb20
Update MD docs
rvasin Jul 11, 2023
ea3d9e9
Add support of YAML configs for decryption
rvasin Jul 18, 2023
12df1b2
Fix MD docs style
rvasin Jul 18, 2023
59570b7
Make encryptValue and decryptValue static
rvasin Jul 18, 2023
c8c6c31
Change Method into method in exceptions
rvasin Jul 18, 2023
5073401
Fix test style with black
rvasin Jul 18, 2023
8649c84
Remove conditional linking
rvasin Jul 20, 2023
8adf57a
Fix text in comments and improve exception handling
rvasin Jul 20, 2023
3798bd6
Replace test by text_to_encrypt
rvasin Jul 21, 2023
0aed62e
Add codec name into exception message
rvasin Jul 21, 2023
10ec069
Improve exception message text
rvasin Jul 21, 2023
1daa26c
Fix black formatting
rvasin Jul 21, 2023
5fb5ba7
Throw exception when several text nodes found in YAML for element node
rvasin Jul 21, 2023
0af869f
Merge branch 'master' into ADQM-822
rvasin Jul 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
rvasin marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -94,6 +94,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 @@ -27,6 +27,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 @@ -40,6 +48,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 @@ -171,6 +182,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 @@ -700,7 +777,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 @@ -755,6 +844,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 @@ -94,7 +94,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 @@ -106,6 +106,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 @@ -124,6 +132,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;
}