Skip to content

Commit 372f8f3

Browse files
authored
Merge pull request #9338 from Icinga/Al2Klimov-patch-3-212
Let new cluster certificates expire after 397 days, not 15 years
2 parents c19a919 + a2817ae commit 372f8f3

File tree

5 files changed

+126
-38
lines changed

5 files changed

+126
-38
lines changed

lib/base/tlsutility.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ std::shared_ptr<X509> CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME
541541
X509 *cert = X509_new();
542542
X509_set_version(cert, 2);
543543
X509_gmtime_adj(X509_get_notBefore(cert), 0);
544-
X509_gmtime_adj(X509_get_notAfter(cert), 365 * 24 * 60 * 60 * 15);
544+
X509_gmtime_adj(X509_get_notAfter(cert), ca ? ROOT_VALID_FOR : LEAF_VALID_FOR);
545545
X509_set_pubkey(cert, pubkey);
546546

547547
X509_set_subject_name(cert, subject);
@@ -670,6 +670,20 @@ std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert)
670670
return CreateCertIcingaCA(pkey.get(), X509_get_subject_name(cert.get()));
671671
}
672672

673+
bool IsCertUptodate(const std::shared_ptr<X509>& cert)
674+
{
675+
time_t now;
676+
time(&now);
677+
678+
/* auto-renew all certificates which were created before 2017 to force an update of the CA,
679+
* because Icinga versions older than 2.4 sometimes create certificates with an invalid
680+
* serial number. */
681+
time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
682+
time_t renewalStart = now + RENEW_THRESHOLD;
683+
684+
return X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1;
685+
}
686+
673687
String CertificateToString(const std::shared_ptr<X509>& cert)
674688
{
675689
BIO *mem = BIO_new(BIO_s_mem());

lib/base/tlsutility.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
namespace icinga
2525
{
2626

27+
const auto ROOT_VALID_FOR = 60 * 60 * 24 * 365 * 15;
28+
const auto LEAF_VALID_FOR = 60 * 60 * 24 * 397;
29+
const auto RENEW_THRESHOLD = 60 * 60 * 24 * 30;
30+
const auto RENEW_INTERVAL = 60 * 60 * 24;
31+
2732
void InitializeOpenSSL();
2833

2934
String GetOpenSSLVersion();
@@ -45,6 +50,7 @@ String CertificateToString(const std::shared_ptr<X509>& cert);
4550
std::shared_ptr<X509> StringToCertificate(const String& cert);
4651
std::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
4752
std::shared_ptr<X509> CreateCertIcingaCA(const std::shared_ptr<X509>& cert);
53+
bool IsCertUptodate(const std::shared_ptr<X509>& cert);
4854

4955
String PBKDF2_SHA1(const String& password, const String& salt, int iterations);
5056
String PBKDF2_SHA256(const String& password, const String& salt, int iterations);

lib/remote/apilistener.cpp

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <boost/asio/ssl/context.hpp>
3131
#include <boost/date_time/posix_time/posix_time_duration.hpp>
3232
#include <boost/system/error_code.hpp>
33+
#include <boost/thread/locks.hpp>
3334
#include <climits>
3435
#include <cstdint>
3536
#include <fstream>
@@ -177,6 +178,30 @@ void ApiListener::OnConfigLoaded()
177178
UpdateSSLContext();
178179
}
179180

181+
std::shared_ptr<X509> ApiListener::RenewCert(const std::shared_ptr<X509>& cert)
182+
{
183+
std::shared_ptr<EVP_PKEY> pubkey (X509_get_pubkey(cert.get()), EVP_PKEY_free);
184+
auto subject (X509_get_subject_name(cert.get()));
185+
auto cacert (GetX509Certificate(GetDefaultCaPath()));
186+
auto newcert (CreateCertIcingaCA(pubkey.get(), subject));
187+
188+
/* verify that the new cert matches the CA we're using for the ApiListener;
189+
* this ensures that the CA we have in /var/lib/icinga2/ca matches the one
190+
* we're using for cluster connections (there's no point in sending a client
191+
* a certificate it wouldn't be able to use to connect to us anyway) */
192+
try {
193+
if (!VerifyCertificate(cacert, newcert, GetCrlPath())) {
194+
Log(LogWarning, "ApiListener")
195+
<< "The CA in '" << GetDefaultCaPath() << "' does not match the CA which Icinga uses "
196+
<< "for its own cluster connections. This is most likely a configuration problem.";
197+
198+
return nullptr;
199+
}
200+
} catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
201+
202+
return newcert;
203+
}
204+
180205
void ApiListener::UpdateSSLContext()
181206
{
182207
namespace ssl = boost::asio::ssl;
@@ -216,7 +241,11 @@ void ApiListener::UpdateSSLContext()
216241
}
217242
}
218243

219-
m_SSLContext = context;
244+
{
245+
boost::unique_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
246+
247+
m_SSLContext = context;
248+
}
220249

221250
for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
222251
for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
@@ -247,6 +276,20 @@ void ApiListener::Start(bool runtimeCreated)
247276

248277
SyncLocalZoneDirs();
249278

279+
m_RenewOwnCertTimer = new Timer();
280+
281+
if (Utility::PathExists(GetIcingaCADir() + "/ca.key")) {
282+
RenewOwnCert();
283+
m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) { RenewOwnCert(); });
284+
} else {
285+
m_RenewOwnCertTimer->OnTimerExpired.connect([this](const Timer * const&) {
286+
JsonRpcConnection::SendCertificateRequest(nullptr, nullptr, String());
287+
});
288+
}
289+
290+
m_RenewOwnCertTimer->SetInterval(RENEW_INTERVAL);
291+
m_RenewOwnCertTimer->Start();
292+
250293
ObjectImpl<ApiListener>::Start(runtimeCreated);
251294

252295
{
@@ -296,6 +339,35 @@ void ApiListener::Start(bool runtimeCreated)
296339
OnMasterChanged(true);
297340
}
298341

342+
void ApiListener::RenewOwnCert()
343+
{
344+
auto certPath (GetDefaultCertPath());
345+
auto cert (GetX509Certificate(certPath));
346+
347+
if (IsCertUptodate(cert)) {
348+
return;
349+
}
350+
351+
Log(LogInformation, "ApiListener")
352+
<< "Our certificate will expire soon, but we own the CA. Renewing.";
353+
354+
cert = RenewCert(cert);
355+
356+
if (!cert) {
357+
return;
358+
}
359+
360+
std::fstream certfp;
361+
auto tempCertPath (Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp));
362+
363+
certfp.exceptions(std::ofstream::failbit | std::ofstream::badbit);
364+
certfp << CertificateToString(cert);
365+
certfp.close();
366+
367+
Utility::RenameFile(tempCertPath, certPath);
368+
UpdateSSLContext();
369+
}
370+
299371
void ApiListener::Stop(bool runtimeDeleted)
300372
{
301373
ObjectImpl<ApiListener>::Stop(runtimeDeleted);
@@ -417,23 +489,25 @@ bool ApiListener::AddListener(const String& node, const String& service)
417489
Log(LogInformation, "ApiListener")
418490
<< "Started new listener on '[" << localEndpoint.address() << "]:" << localEndpoint.port() << "'";
419491

420-
IoEngine::SpawnCoroutine(io, [this, acceptor](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor, m_SSLContext); });
492+
IoEngine::SpawnCoroutine(io, [this, acceptor](asio::yield_context yc) { ListenerCoroutineProc(yc, acceptor); });
421493

422494
UpdateStatusFile(localEndpoint);
423495

424496
return true;
425497
}
426498

427-
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)
499+
void ApiListener::ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server)
428500
{
429501
namespace asio = boost::asio;
430502

431503
auto& io (IoEngine::Get().GetIoContext());
432504

433505
for (;;) {
434506
try {
435-
auto sslConn (Shared<AsioTlsStream>::Make(io, *sslContext));
507+
boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
508+
auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext));
436509

510+
lock.unlock();
437511
server->async_accept(sslConn->lowest_layer(), yc);
438512

439513
auto strand (Shared<asio::io_context::strand>::Make(io));
@@ -486,8 +560,11 @@ void ApiListener::AddConnection(const Endpoint::Ptr& endpoint)
486560
<< "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'";
487561

488562
try {
563+
boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex);
489564
auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext, endpoint->GetName()));
490565

566+
lock.unlock();
567+
491568
Timeout::Ptr timeout(new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(GetConnectTimeout() * 1e6)),
492569
[sslConn, endpoint, host, port](asio::yield_context yc) {
493570
Log(LogCritical, "ApiListener")
@@ -756,11 +833,9 @@ void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoi
756833
}
757834

758835
Zone::Ptr myZone = Zone::GetLocalZone();
836+
auto parent (myZone->GetParent());
759837

760-
if (myZone->GetParent() == eZone) {
761-
Log(LogInformation, "ApiListener")
762-
<< "Requesting new certificate for this Icinga instance from endpoint '" << endpoint->GetName() << "'.";
763-
838+
if (parent == eZone || !parent && eZone == myZone) {
764839
JsonRpcConnection::SendCertificateRequest(aclient, nullptr, String());
765840

766841
if (Utility::PathExists(ApiListener::GetCertificateRequestsDir()))

lib/remote/apilistener.hpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <boost/asio/ip/tcp.hpp>
2222
#include <boost/asio/spawn.hpp>
2323
#include <boost/asio/ssl/context.hpp>
24+
#include <boost/thread/shared_mutex.hpp>
2425
#include <mutex>
2526
#include <set>
2627

@@ -59,6 +60,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
5960
static String GetCaDir();
6061
static String GetCertificateRequestsDir();
6162

63+
std::shared_ptr<X509> RenewCert(const std::shared_ptr<X509>& cert);
6264
void UpdateSSLContext();
6365

6466
static ApiListener::Ptr GetInstance();
@@ -129,6 +131,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
129131

130132
private:
131133
Shared<boost::asio::ssl::context>::Ptr m_SSLContext;
134+
boost::shared_mutex m_SSLContextMutex;
132135

133136
mutable boost::mutex m_AnonymousClientsLock;
134137
mutable boost::mutex m_HttpClientsLock;
@@ -140,6 +143,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
140143
Timer::Ptr m_AuthorityTimer;
141144
Timer::Ptr m_CleanupCertificateRequestsTimer;
142145
Timer::Ptr m_ApiPackageIntegrityTimer;
146+
Timer::Ptr m_RenewOwnCertTimer;
143147

144148
Endpoint::Ptr m_LocalEndpoint;
145149

@@ -162,7 +166,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
162166
boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand,
163167
const Shared<AsioTlsStream>::Ptr& client, const String& hostname, ConnectionRole role
164168
);
165-
void ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server, const Shared<boost::asio::ssl::context>::Ptr& sslContext);
169+
void ListenerCoroutineProc(boost::asio::yield_context yc, const Shared<boost::asio::ip::tcp::acceptor>::Ptr& server);
166170

167171
WorkQueue m_RelayQueue;
168172
WorkQueue m_SyncQueue{0, 4};
@@ -191,6 +195,7 @@ class ApiListener final : public ObjectImpl<ApiListener>
191195

192196
void SyncLocalZoneDirs() const;
193197
void SyncLocalZoneDir(const Zone::Ptr& zone) const;
198+
void RenewOwnCert();
194199

195200
void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient);
196201

lib/remote/jsonrpcconnection-pki.cpp

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
7676
}
7777

7878
if (signedByCA) {
79-
time_t now;
80-
time(&now);
81-
82-
/* auto-renew all certificates which were created before 2017 to force an update of the CA,
83-
* because Icinga versions older than 2.4 sometimes create certificates with an invalid
84-
* serial number. */
85-
time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
86-
time_t renewalStart = now + 30 * 24 * 60 * 60;
87-
88-
if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) {
79+
if (IsCertUptodate(cert)) {
8980

9081
Log(LogInformation, "JsonRpcConnection")
9182
<< "The certificate for CN '" << cn << "' is valid and uptodate. Skipping automated renewal.";
@@ -154,8 +145,6 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
154145
}
155146

156147
std::shared_ptr<X509> newcert;
157-
std::shared_ptr<EVP_PKEY> pubkey;
158-
X509_NAME *subject;
159148
Dictionary::Ptr message;
160149
String ticket;
161150

@@ -206,23 +195,11 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona
206195
}
207196
}
208197

209-
pubkey = std::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
210-
subject = X509_get_subject_name(cert.get());
198+
newcert = listener->RenewCert(cert);
211199

212-
newcert = CreateCertIcingaCA(pubkey.get(), subject);
213-
214-
/* verify that the new cert matches the CA we're using for the ApiListener;
215-
* this ensures that the CA we have in /var/lib/icinga2/ca matches the one
216-
* we're using for cluster connections (there's no point in sending a client
217-
* a certificate it wouldn't be able to use to connect to us anyway) */
218-
try {
219-
if (!VerifyCertificate(cacert, newcert, listener->GetCrlPath())) {
220-
Log(LogWarning, "JsonRpcConnection")
221-
<< "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses "
222-
<< "for its own cluster connections. This is most likely a configuration problem.";
223-
goto delayed_request;
224-
}
225-
} catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */
200+
if (!newcert) {
201+
goto delayed_request;
202+
}
226203

227204
/* Send the signed certificate update. */
228205
Log(LogInformation, "JsonRpcConnection")
@@ -288,6 +265,17 @@ void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& acl
288265

289266
/* Path is empty if this is our own request. */
290267
if (path.IsEmpty()) {
268+
{
269+
Log msg (LogInformation, "JsonRpcConnection");
270+
msg << "Requesting new certificate for this Icinga instance";
271+
272+
if (aclient) {
273+
msg << " from endpoint '" << aclient->GetIdentity() << "'";
274+
}
275+
276+
msg << ".";
277+
}
278+
291279
String ticketPath = ApiListener::GetCertsDir() + "/ticket";
292280

293281
std::ifstream fp(ticketPath.CStr());

0 commit comments

Comments
 (0)