Merge pull request #9179 from Icinga/Al2Klimov-patch-3

Let new cluster certificates expire after 397 days, not 15 years
This commit is contained in:
Julian Brost 2022-04-11 15:29:05 +02:00 committed by GitHub
commit b24a2fa2a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 129 additions and 38 deletions

View File

@ -623,7 +623,7 @@ std::shared_ptr<X509> CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME
X509 *cert = X509_new();
X509_set_version(cert, 2);
X509_gmtime_adj(X509_get_notBefore(cert), 0);
X509_gmtime_adj(X509_get_notAfter(cert), 365 * 24 * 60 * 60 * 15);
X509_gmtime_adj(X509_get_notAfter(cert), ca ? ROOT_VALID_FOR : LEAF_VALID_FOR);
X509_set_pubkey(cert, pubkey);
X509_set_subject_name(cert, subject);
@ -752,6 +752,20 @@ std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert)
return CreateCertIcingaCA(pkey.get(), X509_get_subject_name(cert.get()));
}
bool IsCertUptodate(const std::shared_ptr<X509>& cert)
{
time_t now;
time(&now);
/* auto-renew all certificates which were created before 2017 to force an update of the CA,
* because Icinga versions older than 2.4 sometimes create certificates with an invalid
* serial number. */
time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
time_t renewalStart = now + RENEW_THRESHOLD;
return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1;
}
String CertificateToString(const std::shared_ptr<X509>& cert)
{
BIO *mem = BIO_new(BIO_s_mem());

View File

@ -30,6 +30,11 @@ const char * const DEFAULT_TLS_CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RS
const char * const DEFAULT_TLS_PROTOCOLMIN = "TLSv1.2";
const unsigned int DEFAULT_CONNECT_TIMEOUT = 15;
const auto ROOT_VALID_FOR = 60 * 60 * 24 * 365 * 15;
const auto LEAF_VALID_FOR = 60 * 60 * 24 * 397;
const auto RENEW_THRESHOLD = 60 * 60 * 24 * 30;
const auto RENEW_INTERVAL = 60 * 60 * 24;
void InitializeOpenSSL();
String GetOpenSSLVersion();
@ -55,6 +60,7 @@ String CertificateToString(const std::shared_ptr<X509>& cert);
std::shared_ptr<X509> StringToCertificate(const String& cert);
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert);
bool IsCertUptodate(const std::shared_ptr<X509>& cert);
String PBKDF2_SHA1(const String& password, const String& salt, int iterations);
String PBKDF2_SHA256(const String& password, const String& salt, int iterations);

View File

@ -32,6 +32,7 @@
#include <boost/lexical_cast.hpp>
#include <boost/regex.hpp>
#include <boost/system/error_code.hpp>
#include <boost/thread/locks.hpp>
#include <climits>
#include <cstdint>
#include <fstream>
@ -179,9 +180,39 @@ void ApiListener::OnConfigLoaded()
UpdateSSLContext();
}
std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert)
{
std::shared_ptr<EVP_PKEY> pubkey (X509_get_pubkey(cert.get()), EVP_PKEY_free);
auto subject (X509_get_subject_name(cert.get()));
auto cacert (GetX509Certificate(GetDefaultCaPath()));
auto newcert (CreateCertIcingaCA(pubkey.get(), subject));
/* verify that the new cert matches the CA we're using for the ApiListener;
* this ensures that the CA we have in /var/lib/icinga2/ca matches the one
* we're using for cluster connections (there's no point in sending a client
* a certificate it wouldn't be able to use to connect to us anyway) */
try {
if (!VerifyCertificate(cacert, newcert, GetCrlPath())) {
Log(LogWarning, "ApiListener")
<< "The CA in '" << GetDefaultCaPath() << "' does not match the CA which Icinga uses "
<< "for its own cluster connections. This is most likely a configuration problem.";
return nullptr;
}
} catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
return newcert;
}
void ApiListener::UpdateSSLContext()
{
m_SSLContext = SetupSslContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath(), GetCrlPath(), GetCipherList(), GetTlsProtocolmin(), GetDebugInfo());
auto ctx (SetupSslContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath(), GetCrlPath(), GetCipherList(), GetTlsProtocolmin(), GetDebugInfo()));
{
boost::unique_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
m_SSLContext = std::move(ctx);
}
for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
@ -212,6 +243,20 @@ void ApiListener::Start(bool runtimeCreated)
SyncLocalZoneDirs();
m_RenewOwnCertTimer = new Timer();
if (Utility::PathExists(GetIcingaCADir() + "/ca.key")) {
RenewOwnCert();
m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { RenewOwnCert(); });
} else {
m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
JsonRpcConnection::SendCertificateRequest(nullptr, nullptr, String());
});
}
m_RenewOwnCertTimer->SetInterval(RENEW_INTERVAL);
m_RenewOwnCertTimer->Start();
ObjectImpl<ApiListener>::Start(runtimeCreated);
{
@ -261,6 +306,35 @@ void ApiListener::Start(bool runtimeCreated)
OnMasterChanged(true);
}
void ApiListener::RenewOwnCert()
{
auto certPath (GetDefaultCertPath());
auto cert (GetX509Certificate(certPath));
if (IsCertUptodate(cert)) {
return;
}
Log(LogInformation, "ApiListener")
<< "Our certificate will expire soon, but we own the CA. Renewing.";
cert = RenewCert(cert);
if (!cert) {
return;
}
std::fstream certfp;
auto tempCertPath (Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp));
certfp.exceptions(std::ofstream::failbit | std::ofstream::badbit);
certfp << CertificateToString(cert);
certfp.close();
Utility::RenameFile(tempCertPath, certPath);
UpdateSSLContext();
}
void ApiListener::Stop(bool runtimeDeleted)
{
ObjectImpl<ApiListener>::Stop(runtimeDeleted);
@ -382,14 +456,14 @@ bool ApiListener::AddListener(const String& node, const String& service)
Log(LogInformation, "ApiListener")
<< "Started new listener on '[" << localEndpoint.address() << "]:" << localEndpoint.port() << "'";
IoEngine::SpawnCoroutine(io, [this, acceptor](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor, m_SSLContext); });
IoEngine::SpawnCoroutine(io, [this, acceptor](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor); });
UpdateStatusFile(localEndpoint);
return true;
}
void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server, const Shared<boost::asio::ssl::context>::Ptr& sslContext)
void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server)
{
namespace asio = boost::asio;
@ -418,7 +492,10 @@ void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const Sha
}
}
auto sslConn (Shared<AsioTlsStream>::Make(io, *sslContext));
boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext));
lock.unlock();
sslConn->lowest_layer() = std::move(socket);
auto strand (Shared<asio::io_context::strand>::Make(io));
@ -471,8 +548,11 @@ void ApiListener::AddConnection(const Endpoint::Ptr& endpoint)
<< "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
try {
boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext, endpoint->GetName()));
lock.unlock();
Timeout::Ptr timeout(new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(GetConnectTimeout() * 1e6)),
[sslConn, endpoint, host, port](asio::yield_context yc) {
Log(LogCritical, "ApiListener")
@ -771,11 +851,9 @@ void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoi
}
Zone::Ptr myZone = Zone::GetLocalZone();
auto parent (myZone->GetParent());
if (myZone->GetParent() == eZone) {
Log(LogInformation, "ApiListener")
<< "Requesting new certificate for this Icinga instance from endpoint '" << endpoint->GetName() << "'.";
if (parent == eZone || !parent && eZone == myZone) {
JsonRpcConnection::SendCertificateRequest(aclient, nullptr, String());
if (Utility::PathExists(ApiListener::GetCertificateRequestsDir())) {

View File

@ -21,6 +21,7 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/thread/shared_mutex.hpp>
#include <cstdint>
#include <mutex>
#include <set>
@ -89,6 +90,7 @@ public:
static String GetCaDir();
static String GetCertificateRequestsDir();
std::shared_ptr<X509> RenewCert(const std::shared_ptr<X509>& cert);
void UpdateSSLContext();
static ApiListener::Ptr GetInstance();
@ -159,6 +161,7 @@ protected:
private:
Shared<boost::asio::ssl::context>::Ptr m_SSLContext;
boost::shared_mutex m_SSLContextMutex;
mutable std::mutex m_AnonymousClientsLock;
mutable std::mutex m_HttpClientsLock;
@ -170,6 +173,7 @@ private:
Timer::Ptr m_AuthorityTimer;
Timer::Ptr m_CleanupCertificateRequestsTimer;
Timer::Ptr m_ApiPackageIntegrityTimer;
Timer::Ptr m_RenewOwnCertTimer;
Endpoint::Ptr m_LocalEndpoint;
@ -192,7 +196,7 @@ private:
boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
);
void ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server, const Shared<boost::asio::ssl::context>::Ptr& sslContext);
void ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server);
WorkQueue m_RelayQueue;
WorkQueue m_SyncQueue{0, 4};
@ -221,6 +225,7 @@ private:
void SyncLocalZoneDirs() const;
void SyncLocalZoneDir(const Zone::Ptr& zone) const;
void RenewOwnCert();
void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient);

View File

@ -77,16 +77,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
}
if (signedByCA) {
time_t now;
time(&now);
/* auto-renew all certificates which were created before 2017 to force an update of the CA,
* because Icinga versions older than 2.4 sometimes create certificates with an invalid
* serial number. */
time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
time_t renewalStart = now + 30 * 24 * 60 * 60;
if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) {
if (IsCertUptodate(cert)) {
Log(LogInformation, "JsonRpcConnection")
<< "The certificate for CN '" << cn << "' is valid and uptodate. Skipping automated renewal.";
@ -155,8 +146,6 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
}
std::shared_ptr<X509> newcert;
std::shared_ptr<EVP_PKEY> pubkey;
X509_NAME *subject;
Dictionary::Ptr message;
String ticket;
@ -207,23 +196,11 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
}
}
pubkey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
subject = X509_get_subject_name(cert.get());
newcert = listener->RenewCert(cert);
newcert = CreateCertIcingaCA(pubkey.get(), subject);
/* verify that the new cert matches the CA we're using for the ApiListener;
* this ensures that the CA we have in /var/lib/icinga2/ca matches the one
* we're using for cluster connections (there's no point in sending a client
* a certificate it wouldn't be able to use to connect to us anyway) */
try {
if (!VerifyCertificate(cacert, newcert, listener->GetCrlPath())) {
Log(LogWarning, "JsonRpcConnection")
<< "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses "
<< "for its own cluster connections. This is most likely a configuration problem.";
goto delayed_request;
}
} catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
if (!newcert) {
goto delayed_request;
}
/* Send the signed certificate update. */
Log(LogInformation, "JsonRpcConnection")
@ -289,6 +266,17 @@ void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& acl
/* Path is empty if this is our own request. */
if (path.IsEmpty()) {
{
Log msg (LogInformation, "JsonRpcConnection");
msg << "Requesting new certificate for this Icinga instance";
if (aclient) {
msg << " from endpoint '" << aclient->GetIdentity() << "'";
}
msg << ".";
}
String ticketPath = ApiListener::GetCertsDir() + "/ticket";
std::ifstream fp(ticketPath.CStr());