Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit912237d

Browse files
authored
feat: automatic mTLS certificate regeneration and retry mechanism (#224)
This adds support for automatically recovering from SSL handshake errorswhen certificates expired. When an SSL error occurs, the plugin will nowattempt to execute a configured external command to refreshcertificates. If successful, the SSL context is reloaded and the failedrequest is transparently retried. This improves reliability inenvironments with short-lived or frequently rotating certificates.Netflix requested this, they don't have a reliable mechanism to detectand refresh the certificates before any major disruption in CoderToolbox.
1 parentb7fa471 commit912237d

File tree

14 files changed

+181
-31
lines changed

14 files changed

+181
-31
lines changed

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
###Added
66

77
- application name can now be displayed as the main title page instead of the URL
8+
- automatic mTLS certificate regeneration and retry mechanism
89

910
###Changed
1011

‎src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,14 @@ class CoderRemoteProvider(
414414
* Auto-login only on first the firs run if there is a url & token configured or the auth
415415
* should be done via certificates.
416416
*/
417-
privatefunshouldDoAutoSetup():Boolean= firstRun&& (canAutoLogin()||!settings.requireTokenAuth)
417+
privatefunshouldDoAutoSetup():Boolean= firstRun&& (canAutoLogin()||!settings.requiresTokenAuth)
418418

419419
funcanAutoLogin():Boolean=!context.secrets.tokenFor(context.deploymentUrl).isNullOrBlank()
420420

421421
privatefunonConnect(client:CoderRestClient,cli:CoderCLIManager) {
422422
// Store the URL and token for use next time.
423423
context.settingsStore.updateLastUsedUrl(client.url)
424-
if (context.settingsStore.requireTokenAuth) {
424+
if (context.settingsStore.requiresTokenAuth) {
425425
context.secrets.storeTokenFor(client.url, client.token?:"")
426426
context.logger.info("Deployment URL and token were stored and will be available for automatic connection")
427427
}else {

‎src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.coder.toolbox.sdk.v2.models.Workspace
1919
importcom.coder.toolbox.sdk.v2.models.WorkspaceAgent
2020
importcom.coder.toolbox.settings.SignatureFallbackStrategy.ALLOW
2121
importcom.coder.toolbox.util.InvalidVersionException
22+
importcom.coder.toolbox.util.ReloadableTlsContext
2223
importcom.coder.toolbox.util.SemVer
2324
importcom.coder.toolbox.util.escape
2425
importcom.coder.toolbox.util.escapeSubcommand
@@ -153,7 +154,8 @@ class CoderCLIManager(
153154
}
154155
val okHttpClient=CoderHttpClientBuilder.build(
155156
context,
156-
interceptors
157+
interceptors,
158+
ReloadableTlsContext(context.settingsStore.readOnly().tls)
157159
)
158160

159161
val retrofit=Retrofit.Builder()

‎src/main/kotlin/com/coder/toolbox/sdk/CoderHttpClientBuilder.kt‎

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,19 @@ package com.coder.toolbox.sdk
22

33
importcom.coder.toolbox.CoderToolboxContext
44
importcom.coder.toolbox.util.CoderHostnameVerifier
5-
importcom.coder.toolbox.util.coderSocketFactory
6-
importcom.coder.toolbox.util.coderTrustManagers
5+
importcom.coder.toolbox.util.ReloadableTlsContext
76
importcom.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
87
importokhttp3.Credentials
98
importokhttp3.Interceptor
109
importokhttp3.OkHttpClient
11-
importjavax.net.ssl.X509TrustManager
1210

1311
object CoderHttpClientBuilder {
1412
funbuild(
1513
context:CoderToolboxContext,
16-
interceptors:List<Interceptor>
14+
interceptors:List<Interceptor>,
15+
tlsContext:ReloadableTlsContext
1716
):OkHttpClient {
18-
val settings= context.settingsStore.readOnly()
19-
20-
val socketFactory= coderSocketFactory(settings.tls)
21-
val trustManagers= coderTrustManagers(settings.tls.caPath)
22-
var builder=OkHttpClient.Builder()
17+
val builder=OkHttpClient.Builder()
2318

2419
context.proxySettings.getProxy()?.let { proxy->
2520
context.logger.info("proxy:$proxy")
@@ -43,8 +38,8 @@ object CoderHttpClientBuilder {
4338
.build()
4439
}
4540

46-
builder.sslSocketFactory(socketFactory, trustManagers[0]asX509TrustManager)
47-
.hostnameVerifier(CoderHostnameVerifier(settings.tls.altHostname))
41+
builder.sslSocketFactory(tlsContext.sslSocketFactory, tlsContext.trustManager)
42+
.hostnameVerifier(CoderHostnameVerifier(context.settingsStore.tls.altHostname))
4843
.retryOnConnectionFailure(true)
4944

5045
interceptors.forEach { interceptor->

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.convertors.LoggingConverterFactory
77
importcom.coder.toolbox.sdk.convertors.OSConverter
88
importcom.coder.toolbox.sdk.convertors.UUIDConverter
99
importcom.coder.toolbox.sdk.ex.APIResponseException
10+
importcom.coder.toolbox.sdk.interceptors.CertificateRefreshInterceptor
1011
importcom.coder.toolbox.sdk.interceptors.Interceptors
1112
importcom.coder.toolbox.sdk.v2.CoderV2RestFacade
1213
importcom.coder.toolbox.sdk.v2.models.ApiErrorResponse
@@ -20,6 +21,7 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceBuild
2021
importcom.coder.toolbox.sdk.v2.models.WorkspaceBuildReason
2122
importcom.coder.toolbox.sdk.v2.models.WorkspaceResource
2223
importcom.coder.toolbox.sdk.v2.models.WorkspaceTransition
24+
importcom.coder.toolbox.util.ReloadableTlsContext
2325
importcom.squareup.moshi.Moshi
2426
importokhttp3.OkHttpClient
2527
importretrofit2.Response
@@ -40,6 +42,7 @@ open class CoderRestClient(
4042
valtoken:String?,
4143
privatevalpluginVersion:String ="development",
4244
) {
45+
privatelateinitvar tlsContext:ReloadableTlsContext
4346
privatelateinitvar moshi:Moshi
4447
privatelateinitvar httpClient:OkHttpClient
4548
privatelateinitvar retroRestClient:CoderV2RestFacade
@@ -60,12 +63,17 @@ open class CoderRestClient(
6063
.add(OSConverter())
6164
.add(UUIDConverter())
6265
.build()
66+
67+
tlsContext=ReloadableTlsContext(context.settingsStore.readOnly().tls)
68+
6369
val interceptors= buildList {
64-
if (context.settingsStore.requireTokenAuth) {
70+
if (context.settingsStore.requiresTokenAuth) {
6571
if (token.isNullOrBlank()) {
6672
throwIllegalStateException("Token is required for$url deployment")
6773
}
6874
add(Interceptors.tokenAuth(token))
75+
}elseif (context.settingsStore.requiresMTlsAuth&& context.settingsStore.tls.certRefreshCommand?.isNotBlank()==true) {
76+
add(CertificateRefreshInterceptor(context, tlsContext))
6977
}
7078
add((Interceptors.userAgent(pluginVersion)))
7179
add(Interceptors.externalHeaders(context, url))
@@ -74,7 +82,8 @@ open class CoderRestClient(
7482

7583
httpClient=CoderHttpClientBuilder.build(
7684
context,
77-
interceptors
85+
interceptors,
86+
tlsContext
7887
)
7988

8089
retroRestClient=
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
packagecom.coder.toolbox.sdk.interceptors
2+
3+
importcom.coder.toolbox.CoderToolboxContext
4+
importcom.coder.toolbox.util.ReloadableTlsContext
5+
importokhttp3.Interceptor
6+
importokhttp3.Response
7+
importorg.zeroturnaround.exec.ProcessExecutor
8+
importjavax.net.ssl.SSLHandshakeException
9+
importjavax.net.ssl.SSLPeerUnverifiedException
10+
11+
classCertificateRefreshInterceptor(
12+
privatevalcontext:CoderToolboxContext,
13+
privatevaltlsContext:ReloadableTlsContext
14+
) : Interceptor {
15+
overridefunintercept(chain:Interceptor.Chain):Response {
16+
val request= chain.request()
17+
try {
18+
return chain.proceed(request)
19+
}catch (e:Exception) {
20+
if ((eisSSLHandshakeException|| eisSSLPeerUnverifiedException)&& (e.message?.contains("certificate_expired")==true)) {
21+
val command= context.settingsStore.tls.certRefreshCommand
22+
if (command.isNullOrBlank()) {
23+
throwIllegalStateException(
24+
"Certificate expiration interceptor was set but the refresh command was removed in the meantime",
25+
e
26+
)
27+
}
28+
29+
context.logger.info("SSL handshake exception encountered: certificates expired. Running certificate refresh command:$command")
30+
try {
31+
val result=ProcessExecutor()
32+
.command(command.split("").toList())
33+
.exitValueNormal()
34+
.readOutput(true)
35+
.execute()
36+
context.logger.info("`$command`:${result.outputUTF8()}")
37+
38+
if (result.exitValue==0) {
39+
context.logger.info("Certificate refresh command executed successfully. Reloading SSL certificates.")
40+
tlsContext.reload()
41+
// Retry the request
42+
return chain.proceed(request)
43+
}else {
44+
context.logger.error("Certificate refresh command failed with exit code${result.exitValue}")
45+
}
46+
}catch (ex:Exception) {
47+
context.logger.error(ex,"Failed to execute certificate refresh command")
48+
}
49+
}
50+
throw e
51+
}
52+
}
53+
}

‎src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,12 @@ interface ReadOnlyCoderSettings {
114114
/**
115115
* Whether login should be done with a token
116116
*/
117-
val requireTokenAuth:Boolean
117+
val requiresTokenAuth:Boolean
118+
119+
/**
120+
* Whether the authentication is done with certificates.
121+
*/
122+
val requiresMTlsAuth:Boolean
118123

119124
/**
120125
* Whether to add --disable-autostart to the proxy command. This works
@@ -216,6 +221,12 @@ interface ReadOnlyTLSSettings {
216221
* Coder service does not match the hostname in the TLS certificate.
217222
*/
218223
val altHostname:String?
224+
225+
/**
226+
* Command to run when certificates expire and SSLHandshakeException
227+
* is raised with `Received fatal alert: certificate_expired` as message
228+
*/
229+
val certRefreshCommand:String?
219230
}
220231

221232
enumclassSignatureFallbackStrategy {

‎src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class CoderSettingsStore(
3232
overridevalcertPath:String?,
3333
overridevalkeyPath:String?,
3434
overridevalcaPath:String?,
35-
overridevalaltHostname:String?
35+
overridevalaltHostname:String?,
36+
overridevalcertRefreshCommand:String?
3637
) : ReadOnlyTLSSettings
3738

3839
// Properties implementation
@@ -62,9 +63,11 @@ class CoderSettingsStore(
6263
certPath= store[TLS_CERT_PATH],
6364
keyPath= store[TLS_KEY_PATH],
6465
caPath= store[TLS_CA_PATH],
65-
altHostname= store[TLS_ALTERNATE_HOSTNAME]
66+
altHostname= store[TLS_ALTERNATE_HOSTNAME],
67+
certRefreshCommand= store[TLS_CERT_REFRESH_COMMAND]
6668
)
67-
overrideval requireTokenAuth:Boolean get()= tls.certPath.isNullOrBlank()|| tls.keyPath.isNullOrBlank()
69+
overrideval requiresTokenAuth:Boolean get()= tls.certPath.isNullOrBlank()|| tls.keyPath.isNullOrBlank()
70+
overrideval requiresMTlsAuth:Boolean get()= tls.certPath?.isNotBlank()==true&& tls.keyPath?.isNotBlank()==true
6871
overrideval disableAutostart:Boolean
6972
get()= store[DISABLE_AUTOSTART]?.toBooleanStrictOrNull()?: (getOS()==OS.MAC)
7073
overrideval isSshWildcardConfigEnabled:Boolean

‎src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ internal const val TLS_CA_PATH = "tlsCAPath"
3636

3737
internalconstvalTLS_ALTERNATE_HOSTNAME="tlsAlternateHostname"
3838

39+
internalconstvalTLS_CERT_REFRESH_COMMAND="tlsCertRefreshCommand"
40+
3941
internalconstvalDISABLE_AUTOSTART="disableAutostart"
4042

4143
internalconstvalENABLE_SSH_WILDCARD_CONFIG="enableSshWildcardConfig"

‎src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ open class CoderProtocolHandler(
7070

7171
context.logger.info("Handling$uri...")
7272
val deploymentURL= resolveDeploymentUrl(params)?:return
73-
val token=if (!context.settingsStore.requireTokenAuth)nullelse resolveToken(params)?:return
73+
val token=if (!context.settingsStore.requiresTokenAuth)nullelse resolveToken(params)?:return
7474
val workspaceName= resolveWorkspaceName(params)?:return
7575

7676
suspendfunonConnect(

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp