diff --git a/doc/09-object-types.md b/doc/09-object-types.md index 19e796aff..4ee242019 100644 --- a/doc/09-object-types.md +++ b/doc/09-object-types.md @@ -1410,6 +1410,14 @@ Configuration Attributes: port | Number | **Optional.** Redis port for IcingaDB. Defaults to `6380`. path | String | **Optional.** Redix unix socket path. Can be used instead of `host` and `port` attributes. password | String | **Optional.** Redis auth password for IcingaDB. + enable\_tls | Boolean | **Optional.** Whether to use TLS. + cert\_path | String | **Optional.** Path to the certificate. + key\_path | String | **Optional.** Path to the private key. + ca\_path | String | **Optional.** Path to the CA certificate to use instead of the system's root CAs. + crl\_path | String | **Optional.** Path to the CRL file. + cipher\_list | String | **Optional.** Cipher list that is allowed. For a list of available ciphers run `openssl ciphers`. Defaults to `ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384:AES128-GCM-SHA256`. + tls\_protocolmin | String | **Optional.** Minimum TLS protocol version. Defaults to `TLSv1.2`. + insecure\_noverify | Boolean | **Optional.** Whether not to verify the peer. ### IdoMySqlConnection diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index 1357ed617..02d677ee0 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -144,7 +144,7 @@ static void InitSslContext(const Shared::Ptr& context << "Error loading system's root CAs: " << ERR_peek_error() << ", \"" << errbuf << "\""; BOOST_THROW_EXCEPTION(openssl_error() << boost::errinfo_api_function("SSL_CTX_set_default_verify_paths") - << errinfo_openssl_error(ERR_peek_error()); + << errinfo_openssl_error(ERR_peek_error())); } } else { if (!SSL_CTX_load_verify_locations(sslContext, cakey.CStr(), nullptr)) { diff --git a/lib/icingadb/icingadb.cpp b/lib/icingadb/icingadb.cpp index 7b05364d1..0fac021fc 100644 --- a/lib/icingadb/icingadb.cpp +++ b/lib/icingadb/icingadb.cpp @@ -33,6 +33,18 @@ IcingaDB::IcingaDB() m_PrefixConfigCheckSum = "icinga:checksum:"; } +void IcingaDB::Validate(int types, const ValidationUtils& utils) +{ + ObjectImpl::Validate(types, utils); + + if (!(types & FAConfig)) + return; + + if (GetEnableTls() && GetCertPath().IsEmpty() != GetKeyPath().IsEmpty()) { + BOOST_THROW_EXCEPTION(ValidationError(this, std::vector(), "Validation failed: Either both a client certificate (cert_path) and its private key (key_path) or none of them must be given.")); + } +} + /** * Starts the component. */ @@ -52,7 +64,9 @@ void IcingaDB::Start(bool runtimeCreated) m_WorkQueue.SetExceptionCallback([this](boost::exception_ptr exp) { ExceptionHandler(std::move(exp)); }); - m_Rcon = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex()); + m_Rcon = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(), + GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(), + GetTlsProtocolmin(), GetCipherList(), GetDebugInfo()); m_Rcon->SetConnectedCallback([this](boost::asio::yield_context& yc) { m_WorkQueue.Enqueue([this]() { OnConnectedHandler(); }); }); @@ -63,7 +77,9 @@ void IcingaDB::Start(bool runtimeCreated) if (!ctype) continue; - RedisConnection::Ptr rCon (new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(), m_Rcon)); + RedisConnection::Ptr rCon (new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(), + GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(), + GetTlsProtocolmin(), GetCipherList(), GetDebugInfo(), m_Rcon)); rCon->Start(); m_Rcons[ctype] = std::move(rCon); } @@ -140,6 +156,17 @@ void IcingaDB::Stop(bool runtimeRemoved) ObjectImpl::Stop(runtimeRemoved); } +void IcingaDB::ValidateTlsProtocolmin(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateTlsProtocolmin(lvalue, utils); + + try { + ResolveTlsProtocolVersion(lvalue()); + } catch (const std::exception& ex) { + BOOST_THROW_EXCEPTION(ValidationError(this, { "tls_protocolmin" }, ex.what())); + } +} + void IcingaDB::AssertOnWorkQueue() { ASSERT(m_WorkQueue.IsWorkerThread()); diff --git a/lib/icingadb/icingadb.hpp b/lib/icingadb/icingadb.hpp index 964b837b9..07ed75904 100644 --- a/lib/icingadb/icingadb.hpp +++ b/lib/icingadb/icingadb.hpp @@ -35,9 +35,13 @@ public: static void ConfigStaticInitialize(); + void Validate(int types, const ValidationUtils& utils) override; virtual void Start(bool runtimeCreated) override; virtual void Stop(bool runtimeRemoved) override; +protected: + void ValidateTlsProtocolmin(const Lazy& lvalue, const ValidationUtils& utils) override; + private: class DumpedGlobals { diff --git a/lib/icingadb/icingadb.ti b/lib/icingadb/icingadb.ti index ebc69e8a6..35299573b 100644 --- a/lib/icingadb/icingadb.ti +++ b/lib/icingadb/icingadb.ti @@ -1,6 +1,7 @@ /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ #include "base/configobject.hpp" +#include "base/tlsutility.hpp" library icingadb; @@ -20,6 +21,25 @@ class IcingaDB : ConfigObject [config] String path; [config, no_user_view, no_user_modify] String password; [config] int db_index; + + [config] bool enable_tls { + default {{{ return false; }}} + }; + + [config] bool insecure_noverify { + default {{{ return false; }}} + }; + + [config] String cert_path; + [config] String key_path; + [config] String ca_path; + [config] String crl_path; + [config] String cipher_list { + default {{{ return DEFAULT_TLS_CIPHERS; }}} + }; + [config] String tls_protocolmin { + default {{{ return DEFAULT_TLS_PROTOCOLMIN; }}} + }; }; } diff --git a/lib/icingadb/redisconnection.cpp b/lib/icingadb/redisconnection.cpp index ee1ede89d..9ccdf5d7f 100644 --- a/lib/icingadb/redisconnection.cpp +++ b/lib/icingadb/redisconnection.cpp @@ -4,11 +4,13 @@ #include "base/array.hpp" #include "base/convert.hpp" #include "base/defer.hpp" +#include "base/exception.hpp" #include "base/io-engine.hpp" #include "base/logger.hpp" #include "base/objectlock.hpp" #include "base/string.hpp" #include "base/tcpsocket.hpp" +#include "base/tlsutility.hpp" #include "base/utility.hpp" #include #include @@ -19,23 +21,39 @@ #include #include #include +#include +#include #include using namespace icinga; namespace asio = boost::asio; -RedisConnection::RedisConnection(const String& host, const int port, const String& path, - const String& password, const int db, const RedisConnection::Ptr& parent) : - RedisConnection(IoEngine::Get().GetIoContext(), host, port, path, password, db, parent) +RedisConnection::RedisConnection(const String& host, int port, const String& path, const String& password, int db, + bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath, + const String& tlsProtocolmin, const String& cipherList, DebugInfo di, const RedisConnection::Ptr& parent) + : RedisConnection(IoEngine::Get().GetIoContext(), host, port, path, password, db, + useTls, insecure, certPath, keyPath, caPath, crlPath, tlsProtocolmin, cipherList, std::move(di), parent) { } -RedisConnection::RedisConnection(boost::asio::io_context& io, String host, int port, String path, - String password, int db, const RedisConnection::Ptr& parent) - : m_Host(std::move(host)), m_Port(port), m_Path(std::move(path)), m_Password(std::move(password)), m_DbIndex(db), - m_Connecting(false), m_Connected(false), m_Started(false), m_Strand(io), - m_QueuedWrites(io), m_QueuedReads(io), m_LogStatsTimer(io), m_Parent(parent) +RedisConnection::RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password, + int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath, + String tlsProtocolmin, String cipherList, DebugInfo di, const RedisConnection::Ptr& parent) + : m_Host(std::move(host)), m_Port(port), m_Path(std::move(path)), m_Password(std::move(password)), + m_DbIndex(db), m_CertPath(std::move(certPath)), m_KeyPath(std::move(keyPath)), m_Insecure(insecure), + m_CaPath(std::move(caPath)), m_CrlPath(std::move(crlPath)), m_TlsProtocolmin(std::move(tlsProtocolmin)), + m_CipherList(std::move(cipherList)), m_DebugInfo(std::move(di)), m_Connecting(false), m_Connected(false), + m_Started(false), m_Strand(io), m_QueuedWrites(io), m_QueuedReads(io), m_LogStatsTimer(io), m_Parent(parent) { + if (useTls && m_Path.IsEmpty()) { + UpdateTLSContext(); + } +} + +void RedisConnection::UpdateTLSContext() +{ + m_TLSContext = SetupSslContext(m_CertPath, m_KeyPath, m_CaPath, + m_CrlPath, m_CipherList, m_TlsProtocolmin, m_DebugInfo); } void RedisConnection::Start() @@ -245,12 +263,48 @@ void RedisConnection::Connect(asio::yield_context& yc) for (;;) { try { if (m_Path.IsEmpty()) { - Log(m_Parent ? LogNotice : LogInformation, "IcingaDB") - << "Trying to connect to Redis server (async) on host '" << m_Host << ":" << m_Port << "'"; + if (m_TLSContext) { + Log(m_Parent ? LogNotice : LogInformation, "IcingaDB") + << "Trying to connect to Redis server (async, TLS) on host '" << m_Host << ":" << m_Port << "'"; - auto conn (Shared::Make(m_Strand.context())); - icinga::Connect(conn->next_layer(), m_Host, Convert::ToString(m_Port), yc); - m_TcpConn = std::move(conn); + auto conn (Shared::Make(m_Strand.context(), *m_TLSContext, m_Host)); + auto& tlsConn (conn->next_layer()); + + if (!m_Insecure) { + auto native (tlsConn.native_handle()); + + X509_VERIFY_PARAM_set1_host(SSL_get0_param(native), m_Host.CStr(), 0); + SSL_set_verify(native, SSL_VERIFY_PEER, NULL); + } + + icinga::Connect(conn->lowest_layer(), m_Host, Convert::ToString(m_Port), yc); + tlsConn.async_handshake(tlsConn.client, yc); + + if (!m_Insecure) { + std::shared_ptr cert (tlsConn.GetPeerCertificate()); + + if (!cert) { + BOOST_THROW_EXCEPTION(std::runtime_error( + "Redis didn't present any TLS certificate." + )); + } + + if (!tlsConn.IsVerifyOK()) { + BOOST_THROW_EXCEPTION(std::runtime_error( + "TLS certificate validation failed: " + std::string(tlsConn.GetVerifyError()) + )); + } + } + + m_TlsConn = std::move(conn); + } else { + Log(m_Parent ? LogNotice : LogInformation, "IcingaDB") + << "Trying to connect to Redis server (async) on host '" << m_Host << ":" << m_Port << "'"; + + auto conn (Shared::Make(m_Strand.context())); + icinga::Connect(conn->next_layer(), m_Host, Convert::ToString(m_Port), yc); + m_TcpConn = std::move(conn); + } } else { Log(LogInformation, "IcingaDB") << "Trying to connect to Redis server (async) on unix socket path '" << m_Path << "'"; @@ -560,7 +614,11 @@ void RedisConnection::WriteItem(boost::asio::yield_context& yc, RedisConnection: RedisConnection::Reply RedisConnection::ReadOne(boost::asio::yield_context& yc) { if (m_Path.IsEmpty()) { - return ReadOne(m_TcpConn, yc); + if (m_TLSContext) { + return ReadOne(m_TlsConn, yc); + } else { + return ReadOne(m_TcpConn, yc); + } } else { return ReadOne(m_UnixConn, yc); } @@ -574,7 +632,11 @@ RedisConnection::Reply RedisConnection::ReadOne(boost::asio::yield_context& yc) void RedisConnection::WriteOne(RedisConnection::Query& query, asio::yield_context& yc) { if (m_Path.IsEmpty()) { - WriteOne(m_TcpConn, query, yc); + if (m_TLSContext) { + WriteOne(m_TlsConn, query, yc); + } else { + WriteOne(m_TcpConn, query, yc); + } } else { WriteOne(m_UnixConn, query, yc); } diff --git a/lib/icingadb/redisconnection.hpp b/lib/icingadb/redisconnection.hpp index 9c7aaa880..57d6b63c3 100644 --- a/lib/icingadb/redisconnection.hpp +++ b/lib/icingadb/redisconnection.hpp @@ -10,6 +10,7 @@ #include "base/ringbuffer.hpp" #include "base/shared.hpp" #include "base/string.hpp" +#include "base/tlsstream.hpp" #include "base/value.hpp" #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -69,8 +71,11 @@ namespace icinga SyncConnection = 255 }; - RedisConnection(const String& host, const int port, const String& path, - const String& password = "", const int db = 0, const Ptr& parent = nullptr); + RedisConnection(const String& host, int port, const String& path, const String& password, int db, + bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath, + const String& tlsProtocolmin, const String& cipherList, DebugInfo di, const Ptr& parent = nullptr); + + void UpdateTLSContext(); void Start(); @@ -134,6 +139,8 @@ namespace icinga typedef boost::asio::buffered_stream TcpConn; typedef boost::asio::buffered_stream UnixConn; + Shared::Ptr m_TLSContext; + template static Value ReadRESP(AsyncReadStream& stream, boost::asio::yield_context& yc); @@ -143,8 +150,9 @@ namespace icinga template static void WriteRESP(AsyncWriteStream& stream, const Query& query, boost::asio::yield_context& yc); - RedisConnection(boost::asio::io_context& io, String host, int port, String path, - String password, int db, const Ptr& parent); + RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password, + int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath, + String tlsProtocolmin, String cipherList, DebugInfo di, const Ptr& parent); void Connect(boost::asio::yield_context& yc); void ReadLoop(boost::asio::yield_context& yc); @@ -169,9 +177,19 @@ namespace icinga String m_Password; int m_DbIndex; + String m_CertPath; + String m_KeyPath; + bool m_Insecure; + String m_CaPath; + String m_CrlPath; + String m_TlsProtocolmin; + String m_CipherList; + DebugInfo m_DebugInfo; + boost::asio::io_context::strand m_Strand; Shared::Ptr m_TcpConn; Shared::Ptr m_UnixConn; + Shared::Ptr m_TlsConn; Atomic m_Connecting, m_Connected, m_Started; struct {