Reload SSL certificates on signal SIGHUP
authorShane Synan <digitalcircuit36939@gmail.com>
Mon, 5 Sep 2016 19:19:03 +0000 (14:19 -0500)
committerManuel Nickschas <sputnick@quassel-irc.org>
Tue, 6 Sep 2016 20:41:54 +0000 (22:41 +0200)
Catch SIGHUP, use it to reload configuration (SSL certs), similar to
nginx and other server programs.  This allows easy automation of
reloading certificates, an important factor with services such as
Let's Encrypt.

If reloading certificates fails, the old certificates are kept to
avoid disrupting new connections until the situation is sorted out.

Resolves GH-208.

src/common/quassel.cpp
src/common/quassel.h
src/core/core.cpp
src/core/core.h
src/core/coreapplication.cpp
src/core/coreapplication.h
src/core/sslserver.cpp
src/core/sslserver.h
src/qtui/monoapplication.cpp
src/qtui/monoapplication.h

index 6671bc4..28fb2b0 100644 (file)
@@ -72,6 +72,11 @@ Quassel::Quassel()
     // We catch SIGTERM and SIGINT (caused by Ctrl+C) to graceful shutdown Quassel.
     signal(SIGTERM, handleSignal);
     signal(SIGINT, handleSignal);
+#ifndef Q_OS_WIN
+    // SIGHUP is used to reload configuration (i.e. SSL certificates)
+    // Windows does not support SIGHUP
+    signal(SIGHUP, handleSignal);
+#endif
 }
 
 
@@ -323,6 +328,20 @@ void Quassel::handleSignal(int sig)
         else
             QCoreApplication::quit();
         break;
+#ifndef Q_OS_WIN
+// Windows does not support SIGHUP
+    case SIGHUP:
+        // Most applications use this as the 'configuration reload' command, e.g. nginx uses it for
+        // graceful reloading of processes.
+        if (_instance) {
+            // If the instance exists, reload the configuration
+            quInfo() << "Caught signal" << SIGHUP <<"- reloading configuration";
+            if (_instance->reloadConfig()) {
+                quInfo() << "Successfully reloaded configuration";
+            }
+        }
+        break;
+#endif
     case SIGABRT:
     case SIGSEGV:
 #ifndef Q_OS_WIN
index a2a573c..b705d59 100644 (file)
@@ -144,6 +144,16 @@ protected:
     virtual bool init();
     virtual void quit();
 
+    /**
+     * Requests a reload of relevant runtime configuration.
+     *
+     * Does not reload all configuration, but could catch things such as SSL certificates.  Unless
+     * overridden, simply does nothing.
+     *
+     * @return True if configuration reload successful, otherwise false
+     */
+    virtual bool reloadConfig() { return true; }
+
     inline void setRunMode(RunMode mode);
     inline void setDataDirPaths(const QStringList &paths);
     QStringList findDataDirPaths() const;
index 3eb31e4..0faf769 100644 (file)
@@ -428,6 +428,18 @@ bool Core::sslSupported()
 }
 
 
+bool Core::reloadCerts()
+{
+#ifdef HAVE_SSL
+    SslServer *sslServer = qobject_cast<SslServer *>(&instance()->_server);
+    return sslServer->reloadCerts();
+#else
+    // SSL not supported, don't mark configuration reload as failed
+    return true;
+#endif
+}
+
+
 bool Core::startListening()
 {
     // in mono mode we only start a local port if a port is specified in the cli call
index 8901542..2015f70 100644 (file)
@@ -494,6 +494,14 @@ public:
     static inline QDateTime startTime() { return instance()->_startTime; }
     static inline bool isConfigured() { return instance()->_configured; }
     static bool sslSupported();
+
+    /**
+     * Reloads SSL certificates used for connection with clients
+     *
+     * @return True if certificates reloaded successfully, otherwise false.
+     */
+    static bool reloadCerts();
+
     static QVariantList backendInfo();
 
     static QString setup(const QString &adminUser, const QString &adminPassword, const QString &backend, const QVariantMap &setupData);
index b9422b8..7426f17 100644 (file)
@@ -62,6 +62,17 @@ bool CoreApplicationInternal::init()
 }
 
 
+bool CoreApplicationInternal::reloadConfig()
+{
+    if (_coreCreated) {
+        // Currently, only reloading SSL certificates is supported
+        return Core::reloadCerts();
+    } else {
+        return false;
+    }
+}
+
+
 /*****************************************************************************/
 
 CoreApplication::CoreApplication(int &argc, char **argv)
@@ -94,3 +105,13 @@ bool CoreApplication::init()
     }
     return false;
 }
+
+
+bool CoreApplication::reloadConfig()
+{
+    if (_internal) {
+        return _internal->reloadConfig();
+    } else {
+        return false;
+    }
+}
index dffe905..b350d52 100644 (file)
@@ -38,6 +38,15 @@ public:
 
     bool init();
 
+    /**
+     * Requests a reload of relevant runtime configuration.
+     *
+     * In particular, signals to the Core to reload SSL certificates.
+     *
+     * @return True if configuration reload successful, otherwise false
+     */
+    bool reloadConfig();
+
 private:
     bool _coreCreated;
 };
@@ -52,6 +61,15 @@ public:
 
     bool init();
 
+    /**
+     * Requests a reload of relevant runtime configuration.
+     *
+     * @see Quassel::reloadConfig()
+     *
+     * @return True if configuration reload successful, otherwise false
+     */
+    bool reloadConfig();
+
 private:
     CoreApplicationInternal *_internal;
 };
index 2fc502e..b6becaf 100644 (file)
@@ -36,24 +36,23 @@ SslServer::SslServer(QObject *parent)
     : QTcpServer(parent),
     _isCertValid(false)
 {
+    // Keep track if the SSL warning has been mentioned at least once before
     static bool sslWarningShown = false;
 
-    QString ssl_cert;
-    QString ssl_key;
-
     if(Quassel::isOptionSet("ssl-cert")) {
-        ssl_cert = Quassel::optionValue("ssl-cert");
+        _sslCertPath = Quassel::optionValue("ssl-cert");
     } else {
-        ssl_cert = Quassel::configDirPath() + "quasselCert.pem";
+        _sslCertPath = Quassel::configDirPath() + "quasselCert.pem";
     }
 
     if(Quassel::isOptionSet("ssl-key")) {
-        ssl_key = Quassel::optionValue("ssl-key");
+        _sslKeyPath = Quassel::optionValue("ssl-key");
     } else {
-        ssl_key = ssl_cert;
+        _sslKeyPath = _sslCertPath;
     }
 
-    if (!setCertificate(ssl_cert, ssl_key)) {
+    // Initialize the certificates for first-time usage
+    if (!loadCerts()) {
         if (!sslWarningShown) {
             quWarning()
             << "SslServer: Unable to set certificate file\n"
@@ -95,9 +94,44 @@ void SslServer::incomingConnection(int socketDescriptor)
 }
 
 
+bool SslServer::loadCerts()
+{
+    // Load the certificates specified in the path.  If needed, other prep work can be done here.
+    return setCertificate(_sslCertPath, _sslKeyPath);
+}
+
+
+bool SslServer::reloadCerts()
+{
+    if (loadCerts()) {
+        return true;
+    } else {
+        // Reloading certificates currently only occur in response to a request.  Always print an
+        // error if something goes wrong, in order to simplify checking if it's working.
+        if (isCertValid()) {
+            quWarning()
+            << "SslServer: Unable to reload certificate file, reverting\n"
+            << "          Quassel Core will use the previous key to provide SSL for client connections.\n"
+            << "          Please see http://quassel-irc.org/faq/cert to learn how to enable SSL support.";
+        } else {
+            quWarning()
+            << "SslServer: Unable to reload certificate file\n"
+            << "          Quassel Core will still work, but cannot provide SSL for client connections.\n"
+            << "          Please see http://quassel-irc.org/faq/cert to learn how to enable SSL support.";
+        }
+        return false;
+    }
+}
+
+
 bool SslServer::setCertificate(const QString &path, const QString &keyPath)
 {
-    _isCertValid = false;
+    // Don't reset _isCertValid here, in case an older but valid certificate is still loaded.
+    // Use temporary variables in order to avoid overwriting the existing certificates until
+    // everything is confirmed good.
+    QSslCertificate untestedCert;
+    QList<QSslCertificate> untestedCA;
+    QSslKey untestedKey;
 
     if (path.isEmpty())
         return false;
@@ -122,11 +156,11 @@ bool SslServer::setCertificate(const QString &path, const QString &keyPath)
         return false;
     }
 
-    _cert = certList[0];
+    untestedCert = certList[0];
     certList.removeFirst(); // remove server cert
 
     // store CA and intermediates certs
-    _ca = certList;
+    untestedCA = certList;
 
     if (!certFile.reset()) {
         quWarning() << "SslServer: IO error reading certificate file";
@@ -148,42 +182,47 @@ bool SslServer::setCertificate(const QString &path, const QString &keyPath)
             return false;
         }
 
-        _key = QSslKey(&keyFile, QSsl::Rsa);
+        untestedKey = QSslKey(&keyFile, QSsl::Rsa);
         keyFile.close();
     } else {
-        _key = QSslKey(&certFile, QSsl::Rsa);
+        untestedKey = QSslKey(&certFile, QSsl::Rsa);
     }
 
     certFile.close();
 
-    if (_cert.isNull()) {
+    if (untestedCert.isNull()) {
         quWarning() << "SslServer:" << qPrintable(path) << "contains no certificate data";
         return false;
     }
 
     // We allow the core to offer SSL anyway, so no "return false" here. Client will warn about the cert being invalid.
     const QDateTime now = QDateTime::currentDateTime();
-    if (now < _cert.effectiveDate())
-        quWarning() << "SslServer: Certificate won't be valid before" << _cert.effectiveDate().toString();
+    if (now < untestedCert.effectiveDate())
+        quWarning() << "SslServer: Certificate won't be valid before" << untestedCert.effectiveDate().toString();
 
-    else if (now > _cert.expiryDate())
-        quWarning() << "SslServer: Certificate expired on" << _cert.expiryDate().toString();
+    else if (now > untestedCert.expiryDate())
+        quWarning() << "SslServer: Certificate expired on" << untestedCert.expiryDate().toString();
 
     else { // Qt4's isValid() checks for time range and blacklist; avoid a double warning, hence the else block
 #if QT_VERSION < 0x050000
-        if (!_cert.isValid())
+        if (!untestedCert.isValid())
 #else
-        if (_cert.isBlacklisted())
+        if (untestedCert.isBlacklisted())
 #endif
             quWarning() << "SslServer: Certificate blacklisted";
     }
-    if (_key.isNull()) {
+    if (untestedKey.isNull()) {
         quWarning() << "SslServer:" << qPrintable(keyPath) << "contains no key data";
         return false;
     }
 
     _isCertValid = true;
 
+    // All keys are valid, update the externally visible copy used for new connections.
+    _cert = untestedCert;
+    _ca = untestedCA;
+    _key = untestedKey;
+
     return _isCertValid;
 }
 
index 2b63abd..1f43cc4 100644 (file)
@@ -42,6 +42,16 @@ public:
     virtual inline const QSslKey &key() const { return _key; }
     virtual inline bool isCertValid() const { return _isCertValid; }
 
+    /**
+     * Reloads SSL certificates used for connections
+     *
+     * If this command fails, it will try to maintain the most recent working certificate.  Error
+     * conditions are automatically written to the log.
+     *
+     * @return True if certificates reloaded successfully, otherwise false.
+     */
+    bool reloadCerts();
+
 protected:
 #if QT_VERSION >= 0x050000
     virtual void incomingConnection(qintptr socketDescriptor);
@@ -52,11 +62,25 @@ protected:
     virtual bool setCertificate(const QString &path, const QString &keyPath);
 
 private:
+    /**
+     * Loads SSL certificates used for connections
+     *
+     * If this command fails, it will try to maintain the most recent working certificate.  Will log
+     * specific failure points, but does not offer verbose guidance.
+     *
+     * @return True if certificates loaded successfully, otherwise false.
+     */
+    bool loadCerts();
+
     QLinkedList<QTcpSocket *> _pendingConnections;
     QSslCertificate _cert;
     QSslKey _key;
     QList<QSslCertificate> _ca;
     bool _isCertValid;
+
+    // Used when reloading certificates later
+    QString _sslCertPath; /// Path to the certificate file
+    QString _sslKeyPath;  /// Path to the private key file (may be in same file as above)
 };
 
 
index d6c1bcd..5eb3a5c 100644 (file)
@@ -73,3 +73,13 @@ void MonolithicApplication::startInternalCore()
     connect(connection, SIGNAL(connectToInternalCore(InternalPeer*)), core, SLOT(setupInternalClientSession(InternalPeer*)));
     connect(core, SIGNAL(sessionState(Protocol::SessionState)), connection, SLOT(internalSessionStateReceived(Protocol::SessionState)));
 }
+
+
+bool MonolithicApplication::reloadConfig()
+{
+    if (_internal) {
+        return _internal->reloadConfig();
+    } else {
+        return false;
+    }
+}
index a96051f..6e7b902 100644 (file)
@@ -34,6 +34,15 @@ public:
 
     bool init();
 
+    /**
+     * Requests a reload of relevant runtime configuration.
+     *
+     * @see Quassel::reloadConfig()
+     *
+     * @return True if configuration reload successful, otherwise false
+     */
+    bool reloadConfig();
+
 private slots:
     void startInternalCore();