This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 00a32e2ed15a CAMEL-26828: Close standalone SPIFFE SSL context sources
explicitly (#29632)
00a32e2ed15a is described below
commit 00a32e2ed15a114071f5ca9777c364beef23e5ca
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Nov 11 19:51:00 2026 +0100
CAMEL-26828: Close standalone SPIFFE SSL context sources explicitly (#29632)
camel-spiffe: a standalone SSL context closes its X.509 source explicitly,
defining who owns the cleanup.
Co-authored-by: Codex <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../camel/catalog/docs/spiffe-component.adoc | 6 +
.../src/main/docs/spiffe-component.adoc | 6 +
.../spiffe/SpiffeSSLContextParameters.java | 44 +++++-
.../SpiffeSSLContextParametersLifecycleTest.java | 172 +++++++++++++++++++++
4 files changed, 220 insertions(+), 8 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
index 52027bb966b7..a7ad55e2bb67 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
@@ -138,6 +138,12 @@ Because it extends `SSLContextParameters`, the inherited
configuration is still
`CamelContext` shutdown, and the cached context is invalidated at the same
time so a restarted context rebuilds a
fresh source.
+`SpiffeSSLContextParameters` also implements `AutoCloseable` for standalone
use. When neither the
+`createSSLContext` argument nor the parameters instance supplies a
`CamelContext`, use try-with-resources or call
+`close()` explicitly to release the Workload API watcher. Closing invalidates
the cached context and is idempotent;
+a subsequent `createSSLContext` call creates a new source. Stop using existing
SSL contexts and TLS connections
+before closing their parameters.
+
[tabs]
====
Java::
diff --git a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
index 52027bb966b7..a7ad55e2bb67 100644
--- a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
+++ b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
@@ -138,6 +138,12 @@ Because it extends `SSLContextParameters`, the inherited
configuration is still
`CamelContext` shutdown, and the cached context is invalidated at the same
time so a restarted context rebuilds a
fresh source.
+`SpiffeSSLContextParameters` also implements `AutoCloseable` for standalone
use. When neither the
+`createSSLContext` argument nor the parameters instance supplies a
`CamelContext`, use try-with-resources or call
+`close()` explicitly to release the Workload API watcher. Closing invalidates
the cached context and is idempotent;
+a subsequent `createSSLContext` call creates a new source. Stop using existing
SSL contexts and TLS connections
+before closing their parameters.
+
[tabs]
====
Java::
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
index 873203a951ec..ff335444ff5a 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeSSLContextParameters.java
@@ -56,7 +56,8 @@ import org.apache.camel.util.ObjectHelper;
* {@link #getSecureSocketProtocol()} (default {@code TLSv1.3}).
* <p>
* The underlying {@code X509Source} is created lazily on the first call,
closed when the {@link CamelContext} shuts
- * down, and the cached context is invalidated at the same time so a restarted
context rebuilds a fresh source.
+ * down, and the cached context is invalidated at the same time so a restarted
context rebuilds a fresh source. When
+ * used without a CamelContext, close this instance explicitly, for example
with try-with-resources.
*/
@Metadata(label = "bean",
description = "SSLContextParameters whose SSLContext is backed by
the SPIFFE Workload API: the X.509-SVID and"
@@ -66,7 +67,7 @@ import org.apache.camel.util.ObjectHelper;
+ " still apply.",
annotations = {
"interfaceName=org.apache.camel.support.jsse.SSLContextParameters" })
@Configurer(metadataOnly = true)
-public class SpiffeSSLContextParameters extends SSLContextParameters {
+public class SpiffeSSLContextParameters extends SSLContextParameters
implements AutoCloseable {
private static final String DEFAULT_PROTOCOL = "TLSv1.3";
@@ -90,6 +91,7 @@ public class SpiffeSSLContextParameters extends
SSLContextParameters {
private long initTimeout = 30000L;
private volatile SSLContext sslContext;
+ private X509SourceService sourceService;
public String getSpiffeSocketPath() {
return spiffeSocketPath;
@@ -179,6 +181,7 @@ public class SpiffeSSLContextParameters extends
SSLContextParameters {
}
X509Source source = createX509Source();
+ X509SourceService service = new X509SourceService(source);
try {
SslContextOptions.SslContextOptionsBuilder options
=
SslContextOptions.builder().x509Source(source).sslProtocol(protocol);
@@ -196,11 +199,12 @@ public class SpiffeSSLContextParameters extends
SSLContextParameters {
getSSLEngineConfigurers(spiffeContext),
getSSLSocketFactoryConfigurers(spiffeContext),
getSSLServerSocketFactoryConfigurers(spiffeContext)));
- registerForShutdown(camelContext, source);
+ registerForShutdown(camelContext, service);
+ this.sourceService = service;
this.sslContext = decorated;
return decorated;
} catch (GeneralSecurityException | RuntimeException e) {
- closeQuietly(source);
+ service.stop();
throw e;
}
}
@@ -219,19 +223,35 @@ public class SpiffeSSLContextParameters extends
SSLContextParameters {
}
}
- private void registerForShutdown(CamelContext camelContext, X509Source
source) {
+ private void registerForShutdown(CamelContext camelContext,
X509SourceService service) {
// fall back to the context this instance already knows
(JsseParameters is CamelContextAware), so callers
// that pass null - e.g. some HTTP server factories - still get the
source closed on shutdown
CamelContext context = camelContext != null ? camelContext :
getCamelContext();
if (context != null) {
try {
- context.addService(new X509SourceService(source));
+ context.addService(service);
} catch (Exception e) {
throw new RuntimeCamelException(e);
}
}
}
+ /**
+ * Closes the current Workload API source and invalidates the cached
SSLContext. This is required when no
+ * CamelContext owns this instance, and is safe to call more than once. A
later call to createSSLContext creates a
+ * fresh source; existing TLS connections and SSLContext references must
no longer be used after closing.
+ */
+ @Override
+ public void close() {
+ X509SourceService service;
+ synchronized (this) {
+ service = sourceService;
+ }
+ if (service != null) {
+ service.stop();
+ }
+ }
+
static Set<SpiffeId> parseSpiffeIds(String csv) {
Set<SpiffeId> ids = new LinkedHashSet<>();
for (String part : csv.split(",")) {
@@ -259,6 +279,7 @@ public class SpiffeSSLContextParameters extends
SSLContextParameters {
*/
private final class X509SourceService implements Service {
private final X509Source source;
+ private boolean stopped;
X509SourceService(X509Source source) {
this.source = source;
@@ -271,10 +292,17 @@ public class SpiffeSSLContextParameters extends
SSLContextParameters {
@Override
public void stop() {
- closeQuietly(source);
synchronized (SpiffeSSLContextParameters.this) {
- sslContext = null;
+ if (stopped) {
+ return;
+ }
+ stopped = true;
+ if (sourceService == this) {
+ sourceService = null;
+ sslContext = null;
+ }
}
+ closeQuietly(source);
}
}
}
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersLifecycleTest.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersLifecycleTest.java
new file mode 100644
index 000000000000..fc25eefe744f
--- /dev/null
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSSLContextParametersLifecycleTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.spiffe;
+
+import javax.net.ssl.SSLContext;
+
+import io.spiffe.provider.SpiffeSslContextFactory;
+import io.spiffe.provider.SpiffeSslContextFactory.SslContextOptions;
+import io.spiffe.workloadapi.DefaultX509Source;
+import io.spiffe.workloadapi.DefaultX509Source.X509SourceOptions;
+import org.apache.camel.CamelContext;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+class SpiffeSSLContextParametersLifecycleTest {
+
+ @Test
+ void standaloneSourceIsClosedExactlyOnceAndCanBeRecreated() throws
Exception {
+ try (Sources sources = new Sources(); SpiffeSSLContextParameters
parameters = parameters()) {
+ parameters.close();
+ SSLContext first = parameters.createSSLContext(null);
+ assertThat(parameters.createSSLContext(null)).isSameAs(first);
+ verify(sources.first, never()).close();
+
+ parameters.close();
+ parameters.close();
+ verify(sources.first).close();
+
+ SSLContext second = parameters.createSSLContext(null);
+ assertThat(second).isNotSameAs(first);
+ parameters.close();
+ verify(sources.first).close();
+ verify(sources.second).close();
+ }
+ }
+
+ @Test
+ void nullArgumentUsesTheAssociatedCamelContextForShutdown() throws
Exception {
+ try (Sources sources = new Sources(); CamelContext context = new
DefaultCamelContext()) {
+ context.start();
+ SpiffeSSLContextParameters parameters = parameters();
+ parameters.setCamelContext(context);
+ parameters.createSSLContext(null);
+
+ context.stop();
+ verify(sources.first).close();
+ parameters.close();
+ verify(sources.first).close();
+ }
+ }
+
+ @Test
+ void contextRestartCreatesAFreshSource() throws Exception {
+ try (Sources sources = new Sources(); CamelContext context = new
DefaultCamelContext()) {
+ context.start();
+ SpiffeSSLContextParameters parameters = parameters();
+ SSLContext first = parameters.createSSLContext(context);
+ context.stop();
+ verify(sources.first).close();
+
+ context.start();
+
assertThat(parameters.createSSLContext(context)).isNotSameAs(first);
+ context.stop();
+ verify(sources.first).close();
+ verify(sources.second).close();
+ }
+ }
+
+ @Test
+ void stoppingAnOldContextDoesNotInvalidateARecreatedSource() throws
Exception {
+ try (Sources sources = new Sources(); CamelContext context = new
DefaultCamelContext();
+ CamelContext replacementContext = new DefaultCamelContext();
+ SpiffeSSLContextParameters parameters = parameters()) {
+ context.start();
+ replacementContext.start();
+ parameters.createSSLContext(context);
+ parameters.close();
+ SSLContext replacement =
parameters.createSSLContext(replacementContext);
+
+ context.stop();
+
assertThat(parameters.createSSLContext(replacementContext)).isSameAs(replacement);
+ verify(sources.first).close();
+ verify(sources.second, never()).close();
+ parameters.close();
+ verify(sources.second).close();
+ }
+ }
+
+ @Test
+ void tlsContextCreationFailureClosesTheSourceAndAllowsRetry() throws
Exception {
+ try (Sources sources = new Sources(); SpiffeSSLContextParameters
parameters = parameters()) {
+ sources.factory.when(() ->
SpiffeSslContextFactory.getSslContext(any(SslContextOptions.class)))
+ .thenThrow(new IllegalStateException("TLS initialization
failed"))
+ .thenReturn(SSLContext.getDefault());
+
+ assertThatThrownBy(() -> parameters.createSSLContext(null))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("TLS initialization failed");
+ verify(sources.first).close();
+
+ assertThat(parameters.createSSLContext(null)).isNotNull();
+ parameters.close();
+ verify(sources.first).close();
+ verify(sources.second).close();
+ }
+ }
+
+ @Test
+ void shutdownRegistrationFailureClosesTheSource() throws Exception {
+ try (Sources sources = new Sources(); SpiffeSSLContextParameters
parameters = parameters()) {
+ CamelContext context = mock(CamelContext.class);
+ doThrow(new IllegalStateException("Cannot register
service")).when(context).addService(any());
+
+ assertThatThrownBy(() -> parameters.createSSLContext(context))
+ .isInstanceOf(RuntimeCamelException.class)
+ .hasRootCauseMessage("Cannot register service");
+ parameters.close();
+ verify(sources.first, times(1)).close();
+ }
+ }
+
+ private static SpiffeSSLContextParameters parameters() {
+ SpiffeSSLContextParameters parameters = new
SpiffeSSLContextParameters();
+ parameters.setAcceptAnySpiffeId(true);
+ return parameters;
+ }
+
+ private static final class Sources implements AutoCloseable {
+ private final DefaultX509Source first = mock(DefaultX509Source.class);
+ private final DefaultX509Source second = mock(DefaultX509Source.class);
+ private final MockedStatic<DefaultX509Source> source =
mockStatic(DefaultX509Source.class);
+ private final MockedStatic<SpiffeSslContextFactory> factory =
mockStatic(SpiffeSslContextFactory.class);
+
+ private Sources() throws Exception {
+ source.when(() ->
DefaultX509Source.newSource(any(X509SourceOptions.class))).thenReturn(first,
second);
+ factory.when(() ->
SpiffeSslContextFactory.getSslContext(any(SslContextOptions.class)))
+ .thenReturn(SSLContext.getDefault());
+ }
+
+ @Override
+ public void close() {
+ factory.close();
+ source.close();
+ }
+ }
+}