Skip to content

Conversation

@Dinsmoor
Copy link

Currently, when SSL certificates expire or are renewed (like with Let's Encrypt auto-renewal that expire every few months), the Mumble server requires a full restart to pick up the new certificates. This causes service interruption and disconnects all connected users. A friend of mine runs a HUGE (small) mumble server and was annoyed by this. I've worked on a QT project before (GQRX) so somewhat familiar with how this should go.

The server will check every hour for a new certificate with QTimer, and will try to reload from the configured ssl file paths in murmur.ini if we are within a week of expiry.

This just works for file based certificates for now, so not database stored certs but that would be an obvious fleshing out of this. The cert checking activity is logged, idk if this is too aggressive for you guys.

  • Added qtCertCheck timer and initCertMonitoring() to Server class
  • Added checkCertExpiry() slot that runs hourly checks
  • Added reloadCertFromDisk() method that calls Meta::loadSSLSettings()
  • Only reloads when bUsingMetaCert flag is true (file-based certs)
  • Validates new certificate before accepting
  • Logs success/failure with certificate expiry information

I tested this with some self signed test certs, and had some test code to shorten the expiry checking (not in this commit) - started with a certificate expiring in a day, triggering our check for a replacement. I replaced the cert on the disk with a new 90-day expiry one, and the next check detected the change and reloaded the certs successfully. So no restart or connection drops needed, new connects will use the new cert.

Build configuration used for testing (I didn't want to install all the dependancies): cmake -Dserver=ON -Dclient=OFF -Denable-mysql=OFF -Denable-postgresql=OFF -Dzeroconf=OFF -Dice=OFF

User-Visible Changes

  • Server logs certificate expiry checks hourly (only visible when cert is expiring soon)
  • When certificate expires within 7 days: logs reload attempt and result
  • New connections automatically use refreshed certificates after successful reload
  • Existing connections remain unaffected

Future Enhancements

  • Could be extended to support database-stored certificates
  • Check interval and expiry threshold could be made configurable

Checks

Just a note here: I did use Claude Code for a lot of this, making sure I complied with the commit guidelines and the development guide, so if the formatting or structure seems unusually polished for a first-time contributor, that's why. I can write CPP just fine on my own (cope 😭 ), I'm just 10x faster with this tool, and 100x faster with getting familiar with a codebase. Happy to adjust anything that doesn't fit the project's style or conventions, but I did use clang-format and supervised the demon machine throughout the entire thing. Tested compilation on master before making any edits, made my edits, tested compilation, added some test code and swapped out certs (as described above) compiled and tested again, removed the test code to force it to trigger without waiting a few hours, and compiled and tested again, removed the test code, compiled and well, here we are.

Speaking of, changes to spacing of server.cpp is just from clang-format.

Thanks!

Implements periodic certificate expiry monitoring and automatic reloading
from disk when certificates are near expiration. This eliminates the need
for server restarts when SSL certificates are renewed (e.g., Let's Encrypt
auto-renewal).

The server checks certificate expiry every hour via QTimer. When a
certificate expires within 7 days or has already expired, it attempts to
reload from the configured sslCert/sslKey file paths in murmur.ini.

Currently only supports file-based certificates (not database-stored).
All reload operations are logged.

Implementation:
- Added qtCertCheck timer and initCertMonitoring() to Server class
- Added checkCertExpiry() slot for hourly expiry checks
- Added reloadCertFromDisk() method that calls Meta::loadSSLSettings()
- Only reloads when bUsingMetaCert is true (file-based certificates)
- Validates new certificate before accepting
- Safe fallback keeps current certificate if reload fails

New connections automatically use refreshed certificates after successful
reload. Existing connections remain unaffected.

Testing was performed with self-signed certificates expiring in 1 day.
Certificate was replaced on disk with new 90-day cert. Next hourly check
detected change and reloaded successfully without restart or disconnects.

Build config: cmake -Dserver=ON -Dclient=OFF -Denable-mysql=OFF
-Denable-postgresql=OFF -Dzeroconf=OFF -Dice=OFF
@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Walkthrough

These changes add certificate monitoring capabilities to the Server class. A Qt timer triggers hourly checks via the checkCertExpiry() method, which verifies if the qscCert will expire within 7 days or has already expired. If expiry is detected, reloadCertFromDisk() is called to reload the certificate from the file system. The reload method includes safety mechanisms: backing up current certificates before replacement and restoring them if the reload fails. The initCertMonitoring() method sets up the timer and is called during Server initialization in the constructor.

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "SSL Certificate Hot-Reload for Mumble Server" is a concise, single-sentence description that clearly and specifically summarizes the primary change introduced by this pull request. The title directly corresponds to the main objective: adding automatic certificate hot-reload functionality to the Mumble server, enabling certificate updates without requiring a full server restart. The title is neither vague nor misleading and accurately reflects the three new methods (initCertMonitoring, checkCertExpiry, reloadCertFromDisk) and functionality added to the Server class.
Description Check ✅ Passed The pull request description meets the repository template requirements by including the "### Checks" section with the commit guidelines checkbox explicitly marked as complete ([x]). Beyond the minimal template requirement, the description provides comprehensive context including the problem statement, detailed implementation specifics, testing methodology with specific build configurations, user-visible changes, and future enhancement suggestions. The author also transparently discloses the use of Claude Code and clang-format for development, demonstrating care for code quality and project standards.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d5c987 and 36e7b5c.

📒 Files selected for processing (3)
  • src/murmur/Cert.cpp (1 hunks)
  • src/murmur/Server.cpp (6 hunks)
  • src/murmur/Server.h (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/murmur/Server.cpp (1)
src/murmur/Cert.cpp (2)
  • initCertMonitoring (218-222)
  • initCertMonitoring (218-218)
src/murmur/Server.h (1)
src/murmur/Cert.cpp (6)
  • initCertMonitoring (218-222)
  • initCertMonitoring (218-218)
  • checkCertExpiry (224-255)
  • checkCertExpiry (224-224)
  • reloadCertFromDisk (257-297)
  • reloadCertFromDisk (257-257)
src/murmur/Cert.cpp (1)
src/murmur/Server.cpp (4)
  • log (1367-1370)
  • log (1367-1367)
  • log (1372-1380)
  • log (1372-1372)
🔇 Additional comments (3)
src/murmur/Cert.cpp (1)

218-222: LGTM - Timer setup is straightforward.

The hourly certificate check interval is appropriate for the stated goal of detecting expiring certificates within a 7-day window. The PR objectives mention making the interval configurable as a future enhancement, which is reasonable.

src/murmur/Server.h (1)

213-215: LGTM - Declarations are correct.

The new certificate monitoring API is well-structured:

  • qtCertCheck timer is appropriately declared as a value member
  • checkCertExpiry() is correctly declared as a slot for Qt signal/slot connection
  • reloadCertFromDisk() has clear boolean return semantics for success/failure

The public visibility of these methods allows external control over certificate monitoring if needed.

Also applies to: 230-230, 241-241

src/murmur/Server.cpp (1)

257-257: ****

The qtCertCheck timer is an embedded member variable in the Server class, not a heap-allocated pointer. Member variables are automatically destroyed via C++ RAII when the containing object is destroyed—the QTimer destructor will stop the timer synchronously. No explicit stop() call is needed or recommended in the destructor, and no race conditions exist with this pattern. The code is correct as-is.

Likely an incorrect or invalid review comment.

Comment on lines 224 to 255
void Server::checkCertExpiry() {
if (qscCert.isNull()) {
return;
}

QDateTime now = QDateTime::currentDateTime();
QDateTime expiryDate = qscCert.expiryDate();
qint64 daysUntilExpiry = now.daysTo(expiryDate);

// Check if certificate expires within 7 days or has already expired
if (daysUntilExpiry <= 7 && daysUntilExpiry >= 0) {
log(QString("Certificate expires in %1 days, attempting to reload from disk").arg(daysUntilExpiry));

if (reloadCertFromDisk()) {
log("Successfully reloaded certificate from disk");
// Update expiry date after reload
expiryDate = qscCert.expiryDate();
daysUntilExpiry = now.daysTo(expiryDate);
log(QString("New certificate expires in %1 days").arg(daysUntilExpiry));
} else {
log("Failed to reload certificate from disk, keeping current certificate");
}
} else if (daysUntilExpiry < 0) {
log(QString("Certificate has expired %1 days ago, attempting to reload from disk").arg(-daysUntilExpiry));

if (reloadCertFromDisk()) {
log("Successfully reloaded certificate from disk");
} else {
log("Failed to reload certificate from disk, keeping current expired certificate");
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Add thread synchronization for certificate access.

This function reads and updates qscCert, qskKey, and qlIntermediates (via reloadCertFromDisk()) without holding any locks. Given that Server inherits from QThread and has qrwlVoiceThread for synchronizing access to shared data, certificate members should also be protected.

Consider:

  1. New client connections (in newClient() around line 1435-1436) read qscCert and qskKey to configure SSL
  2. The voice thread may indirectly access certificate information
  3. Concurrent calls to checkCertExpiry() could cause data races

Wrap certificate access with appropriate locking:

 void Server::checkCertExpiry() {
+	QReadLocker lock(&qrwlVoiceThread);
+	
 	if (qscCert.isNull()) {
 		return;
 	}

 	QDateTime now          = QDateTime::currentDateTime();
 	QDateTime expiryDate   = qscCert.expiryDate();
 	qint64 daysUntilExpiry = now.daysTo(expiryDate);

+	lock.unlock();
+
 	// Check if certificate expires within 7 days or has already expired
 	if (daysUntilExpiry <= 7 && daysUntilExpiry >= 0) {

And update reloadCertFromDisk() to acquire a write lock when modifying certificates.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/murmur/Cert.cpp around lines 224 to 255, checkCertExpiry() accesses and
updates shared certificate state (qscCert, qskKey, qlIntermediates via
reloadCertFromDisk()) without locking; wrap all reads of qscCert/expiryDate and
the logic that may call reloadCertFromDisk() in a read or upgradeable lock using
the existing qrwlVoiceThread (acquire a read lock for expiry checks, upgrade to
a write lock before calling reloadCertFromDisk or modifying certificate
variables), and change reloadCertFromDisk() to acquire the write lock
(qrwlVoiceThread.lockForWrite()/unlock) while it replaces
qscCert/qskKey/qlIntermediates to prevent races with newClient() and other
threads that read these members.

Comment on lines 257 to 297
bool Server::reloadCertFromDisk() {
// Only reload if we're using Meta certificate (which is loaded from file)
if (!bUsingMetaCert) {
// This server has a database-stored certificate, cannot reload from disk
return false;
}

// Store current certificate as backup
QSslCertificate oldCert = qscCert;
QSslKey oldKey = qskKey;
QList< QSslCertificate > oldInter = qlIntermediates;

// Force Meta to reload its SSL settings from disk
if (!Meta::mp->loadSSLSettings()) {
log("Failed to reload SSL settings from murmur.ini");
return false;
}

// Try to reload from Meta's certificate
qskKey = Meta::mp->qskKey;
qscCert = Meta::mp->qscCert;
qlIntermediates = Meta::mp->qlIntermediates;

// Verify that the reloaded certificate is valid and different
if (qscCert.isNull() || qskKey.isNull()) {
// Reload failed, restore old certificate
qscCert = oldCert;
qskKey = oldKey;
qlIntermediates = oldInter;
return false;
}

// Check if certificate actually changed
if (qscCert == oldCert) {
// Certificate hasn't changed on disk
return false;
}

// Successfully reloaded new certificate
return true;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Add validation and null pointer checks.

Multiple safety issues in the certificate reload logic:

  1. Null pointer dereference risk: Meta::mp is accessed without null check (lines 270, 276-278). If Meta is not initialized, this will crash.

  2. Missing certificate validation: After reloading, the code only checks if the certificate and key are null (line 281) but doesn't verify:

    • The key actually matches the certificate (should use isKeyForCert() from line 24)
    • The new certificate isn't already expired
    • The certificate is properly formed
  3. Thread safety: Same issue as checkCertExpiry() - certificate members are modified without locking.

Apply these fixes:

 bool Server::reloadCertFromDisk() {
 	// Only reload if we're using Meta certificate (which is loaded from file)
 	if (!bUsingMetaCert) {
 		// This server has a database-stored certificate, cannot reload from disk
 		return false;
 	}

+	if (!Meta::mp) {
+		log("Meta not initialized, cannot reload certificate");
+		return false;
+	}
+
 	// Store current certificate as backup
 	QSslCertificate oldCert           = qscCert;
 	QSslKey oldKey                    = qskKey;
 	QList< QSslCertificate > oldInter = qlIntermediates;

 	// Force Meta to reload its SSL settings from disk
 	if (!Meta::mp->loadSSLSettings()) {
 		log("Failed to reload SSL settings from murmur.ini");
 		return false;
 	}

+	QWriteLocker lock(&qrwlVoiceThread);
+
 	// Try to reload from Meta's certificate
 	qskKey          = Meta::mp->qskKey;
 	qscCert         = Meta::mp->qscCert;
 	qlIntermediates = Meta::mp->qlIntermediates;

 	// Verify that the reloaded certificate is valid and different
 	if (qscCert.isNull() || qskKey.isNull()) {
 		// Reload failed, restore old certificate
 		qscCert         = oldCert;
 		qskKey          = oldKey;
 		qlIntermediates = oldInter;
 		return false;
 	}

+	// Verify the key matches the certificate
+	if (!isKeyForCert(qskKey, qscCert)) {
+		log("Reloaded certificate and key do not match");
+		qscCert         = oldCert;
+		qskKey          = oldKey;
+		qlIntermediates = oldInter;
+		return false;
+	}
+
+	// Check if new certificate is already expired
+	if (qscCert.expiryDate() < QDateTime::currentDateTime()) {
+		log("Reloaded certificate is already expired");
+		qscCert         = oldCert;
+		qskKey          = oldKey;
+		qlIntermediates = oldInter;
+		return false;
+	}
+
 	// Check if certificate actually changed
 	if (qscCert == oldCert) {
 		// Certificate hasn't changed on disk
 		return false;
 	}

 	// Successfully reloaded new certificate
 	return true;
 }
🤖 Prompt for AI Agents
In src/murmur/Cert.cpp around lines 257-297, add null-pointer, validation and
locking: first check Meta::mp is not null before calling loadSSLSettings or
accessing its members and return false with a log if it is null; wrap all
reads/writes to qscCert, qskKey and qlIntermediates with the same mutex used
elsewhere for certificate state (use the appropriate lock/guard to ensure
thread-safety); after copying Meta::mp->qscCert/qskKey/qlIntermediates validate
the new certificate by ensuring it is not null, the key matches the certificate
(call isKeyForCert or the existing helper), the certificate parses correctly and
is not expired (check its effective/expiry dates), and if any validation fails
restore oldCert/oldKey/oldInter, unlock and return false; only return true after
successful validation and replacing the state while holding the lock.

The checkCertExpiry() function was accessing shared certificate state
(qscCert, qskKey, qlIntermediates) without any locking, creating a race
condition with newClient() and other threads that read these values.

Additionally, the initial implementation created a new mutex (qmCert)
instead of using the existing qrwlVoiceThread read-write lock that
protects certificate state throughout the codebase. Using two different
locks to protect the same data provides no protection.

Changes:
- checkCertExpiry() now uses QReadLocker(&qrwlVoiceThread) when reading
  certificate state to check expiry dates
- reloadCertFromDisk() uses QReadLocker to safely copy old state for
  comparison, then lockForWrite()/unlock() for atomic certificate updates
- Removed qmCert mutex entirely in favor of existing qrwlVoiceThread lock

This ensures all certificate access uses a single, consistent locking
mechanism and prevents data races during hot-reload operations.
@Dinsmoor
Copy link
Author

Wow that coderabbit review demon machine slaps hahahaha.
Alright, that's why I was coping about knowing anything about cpp 👉 😏 💥

@hamilton5
Copy link

hamilton5 commented Oct 29, 2025

The server only needs the SIGUSR1 signal to reload certs. You do not need to restart and interrupt users. And if you have systemd you can run reload as that's what it does. I just run systemctl reload in my lets-encrypt deploy script.

https://github.com/search?q=repo%3Amumble-voip%2Fmumble+SIGUSr1&type=code

@Dinsmoor
Copy link
Author

Dinsmoor commented Oct 29, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants