Skip to content

Commit 29c94c6

Browse files
fioan89zedkipp
andauthored
fix: lenient mTLS cert reload (#279)
A Coder customer reported that the cert refresh command can return 1 while still generating new certs. Right now Coder Toolbox does not reload the certs if the refresh command exits with a status other than 0. - resolves #276 --------- Co-authored-by: Zach <3724288+zedkipp@users.noreply.github.com>
1 parent 370d54f commit 29c94c6

5 files changed

Lines changed: 51 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Changed
6+
7+
- mTLS connections no longer disconnect when the certificate refresh command exits with a non-zero code
8+
59
## 0.8.5 - 2026-02-03
610

711
### Added

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.8.5
1+
version=0.8.6
22
group=com.coder.toolbox
33
name=coder-toolbox

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -368,16 +368,12 @@ open class CoderRestClient(
368368
return@withContext try {
369369
val result = ProcessExecutor()
370370
.command(command.split(" ").toList())
371-
.exitValueNormal()
371+
.exitValueAny()
372372
.readOutput(true)
373373
.execute()
374-
375-
if (result.exitValue == 0) {
374+
if (tlsContext.reload()) {
376375
context.logger.info("Certificate refresh successful. Reloading TLS and evicting pool.")
377-
tlsContext.reload()
378-
379-
// This is the "Magic Fix":
380-
// It forces OkHttp to close the broken HTTP/2 connection.
376+
// forces OkHttp to close the broken HTTP/2 connection.
381377
httpClient.connectionPool.evictAll()
382378
return@withContext true
383379
} else {

src/main/kotlin/com/coder/toolbox/util/Hash.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import java.io.InputStream
55
import java.security.DigestInputStream
66
import java.security.MessageDigest
77

8+
private const val BUFFER_SIZE = 8192
9+
810
fun ByteArray.toHex() = joinToString(separator = "") { byte -> "%02x".format(byte) }
911

1012
/**
1113
* Return the SHA-1 for the provided stream.
1214
*/
13-
@Suppress("ControlFlowWithEmptyBody")
1415
fun sha1(stream: InputStream): String {
1516
val md = MessageDigest.getInstance("SHA-1")
16-
val dis = DigestInputStream(BufferedInputStream(stream), md)
17-
stream.use {
18-
while (dis.read() != -1) {
17+
DigestInputStream(BufferedInputStream(stream), md).use { dis ->
18+
val buffer = ByteArray(BUFFER_SIZE)
19+
while (dis.read(buffer) != -1) {
20+
// Read until EOF
1921
}
2022
}
2123
return md.digest().toHex()

src/main/kotlin/com/coder/toolbox/util/TLS.kt

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -284,16 +284,29 @@ class MergedSystemTrustManger(private val otherTrustManager: X509TrustManager) :
284284
class ReloadableX509TrustManager(
285285
private val caPath: String?,
286286
) : X509TrustManager {
287+
private var lastHash: String? = null
288+
287289
@Volatile
288290
private var delegate: X509TrustManager = loadTrustManager()
289291

290292
private fun loadTrustManager(): X509TrustManager {
293+
if (!caPath.isNullOrBlank()) {
294+
lastHash = sha1(FileInputStream(expand(caPath)))
295+
}
291296
val trustManagers = coderTrustManagers(caPath)
292297
return trustManagers.first { it is X509TrustManager } as X509TrustManager
293298
}
294299

295-
fun reload() {
296-
delegate = loadTrustManager()
300+
fun reload(): Boolean {
301+
if (caPath.isNullOrBlank()) {
302+
return false
303+
}
304+
val newHash = sha1(FileInputStream(expand(caPath)))
305+
if (lastHash != newHash) {
306+
delegate = loadTrustManager()
307+
return true
308+
}
309+
return false
297310
}
298311

299312
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
@@ -312,15 +325,31 @@ class ReloadableX509TrustManager(
312325
class ReloadableSSLSocketFactory(
313326
private val settings: ReadOnlyTLSSettings,
314327
) : SSLSocketFactory() {
328+
private var lastCertHash: String? = null
329+
private var lastKeyHash: String? = null
330+
315331
@Volatile
316332
private var delegate: SSLSocketFactory = loadSocketFactory()
317333

318334
private fun loadSocketFactory(): SSLSocketFactory {
335+
if (!settings.certPath.isNullOrBlank() && !settings.keyPath.isNullOrBlank()) {
336+
lastCertHash = sha1(FileInputStream(expand(settings.certPath!!)))
337+
lastKeyHash = sha1(FileInputStream(expand(settings.keyPath!!)))
338+
}
319339
return coderSocketFactory(settings)
320340
}
321341

322-
fun reload() {
323-
delegate = loadSocketFactory()
342+
fun reload(): Boolean {
343+
if (settings.certPath.isNullOrBlank() || settings.keyPath.isNullOrBlank()) {
344+
return false
345+
}
346+
val newCertHash = sha1(FileInputStream(expand(settings.certPath!!)))
347+
val newKeyHash = sha1(FileInputStream(expand(settings.keyPath!!)))
348+
if (lastCertHash != newCertHash || lastKeyHash != newKeyHash) {
349+
delegate = loadSocketFactory()
350+
return true
351+
}
352+
return false
324353
}
325354

326355
override fun getDefaultCipherSuites(): Array<String> = delegate.defaultCipherSuites
@@ -349,8 +378,9 @@ class ReloadableTlsContext(
349378
val sslSocketFactory = ReloadableSSLSocketFactory(settings)
350379
val trustManager = ReloadableX509TrustManager(settings.caPath)
351380

352-
fun reload() {
353-
sslSocketFactory.reload()
354-
trustManager.reload()
381+
fun reload(): Boolean {
382+
val socketFactoryReloaded = sslSocketFactory.reload()
383+
val trustManagerReloaded = trustManager.reload()
384+
return socketFactoryReloaded || trustManagerReloaded
355385
}
356386
}

0 commit comments

Comments
 (0)