|
30 | 30 | #include <boost/asio/ssl/context.hpp> |
31 | 31 | #include <boost/date_time/posix_time/posix_time_duration.hpp> |
32 | 32 | #include <boost/system/error_code.hpp> |
| 33 | +#include <boost/thread/locks.hpp> |
33 | 34 | #include <climits> |
34 | 35 | #include <cstdint> |
35 | 36 | #include <fstream> |
@@ -177,6 +178,30 @@ void ApiListener::OnConfigLoaded() |
177 | 178 | UpdateSSLContext(); |
178 | 179 | } |
179 | 180 |
|
| 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 | + |
180 | 205 | void ApiListener::UpdateSSLContext() |
181 | 206 | { |
182 | 207 | namespace ssl = boost::asio::ssl; |
@@ -216,7 +241,11 @@ void ApiListener::UpdateSSLContext() |
216 | 241 | } |
217 | 242 | } |
218 | 243 |
|
219 | | - m_SSLContext = context; |
| 244 | + { |
| 245 | + boost::unique_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex); |
| 246 | + |
| 247 | + m_SSLContext = context; |
| 248 | + } |
220 | 249 |
|
221 | 250 | for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) { |
222 | 251 | for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) { |
@@ -247,6 +276,20 @@ void ApiListener::Start(bool runtimeCreated) |
247 | 276 |
|
248 | 277 | SyncLocalZoneDirs(); |
249 | 278 |
|
| 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 | + |
250 | 293 | ObjectImpl<ApiListener>::Start(runtimeCreated); |
251 | 294 |
|
252 | 295 | { |
@@ -296,6 +339,35 @@ void ApiListener::Start(bool runtimeCreated) |
296 | 339 | OnMasterChanged(true); |
297 | 340 | } |
298 | 341 |
|
| 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 | + |
299 | 371 | void ApiListener::Stop(bool runtimeDeleted) |
300 | 372 | { |
301 | 373 | ObjectImpl<ApiListener>::Stop(runtimeDeleted); |
@@ -417,23 +489,25 @@ bool ApiListener::AddListener(const String& node, const String& service) |
417 | 489 | Log(LogInformation, "ApiListener") |
418 | 490 | << "Started new listener on '[" << localEndpoint.address() << "]:" << localEndpoint.port() << "'"; |
419 | 491 |
|
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); }); |
421 | 493 |
|
422 | 494 | UpdateStatusFile(localEndpoint); |
423 | 495 |
|
424 | 496 | return true; |
425 | 497 | } |
426 | 498 |
|
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) |
428 | 500 | { |
429 | 501 | namespace asio = boost::asio; |
430 | 502 |
|
431 | 503 | auto& io (IoEngine::Get().GetIoContext()); |
432 | 504 |
|
433 | 505 | for (;;) { |
434 | 506 | 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)); |
436 | 509 |
|
| 510 | + lock.unlock(); |
437 | 511 | server->async_accept(sslConn->lowest_layer(), yc); |
438 | 512 |
|
439 | 513 | auto strand (Shared<asio::io_context::strand>::Make(io)); |
@@ -486,8 +560,11 @@ void ApiListener::AddConnection(const Endpoint::Ptr& endpoint) |
486 | 560 | << "Reconnecting to endpoint '" << endpoint->GetName() << "' via host '" << host << "' and port '" << port << "'"; |
487 | 561 |
|
488 | 562 | try { |
| 563 | + boost::shared_lock<decltype(m_SSLContextMutex)> lock (m_SSLContextMutex); |
489 | 564 | auto sslConn (Shared<AsioTlsStream>::Make(io, *m_SSLContext, endpoint->GetName())); |
490 | 565 |
|
| 566 | + lock.unlock(); |
| 567 | + |
491 | 568 | Timeout::Ptr timeout(new Timeout(strand->context(), *strand, boost::posix_time::microseconds(int64_t(GetConnectTimeout() * 1e6)), |
492 | 569 | [sslConn, endpoint, host, port](asio::yield_context yc) { |
493 | 570 | Log(LogCritical, "ApiListener") |
@@ -756,11 +833,9 @@ void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoi |
756 | 833 | } |
757 | 834 |
|
758 | 835 | Zone::Ptr myZone = Zone::GetLocalZone(); |
| 836 | + auto parent (myZone->GetParent()); |
759 | 837 |
|
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) { |
764 | 839 | JsonRpcConnection::SendCertificateRequest(aclient, nullptr, String()); |
765 | 840 |
|
766 | 841 | if (Utility::PathExists(ApiListener::GetCertificateRequestsDir())) |
|
0 commit comments