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 c20e20760802 CAMEL-24733: camel-spiffe - add integration tests against
a real SPIRE agent (#27429)
c20e20760802 is described below
commit c20e207608023b095bd2a45fbd416a404bf5d5ea
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Oct 6 15:17:00 2026 +0200
CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE
agent (#27429)
* CAMEL-24733: camel-spiffe - add integration tests against a real SPIRE
agent
Add a camel-test-infra-spiffe module (SPIRE server + agent exposing the
Workload API) and integration tests that fetch an X509-SVID and a JWT-SVID,
validate a JWT-SVID and reject one minted for another audience, and complete a
real mutual-TLS handshake through SpiffeSSLContextParameters (allow-listed peer
accepted, non-allow-listed refused).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24733: make the mTLS deny assertion robust to a connection-reset
refusal
A rejected mutual-TLS handshake surfaces as an SSLHandshakeException on
some JSSE stacks and as a plain connection-reset SocketException on others
(seen in CI); assert the refusal fails with an IOException (the common
supertype) rather than requiring an SSLException specifically, and unwrap the
server thread's ExecutionException so its underlying cause is what propagates.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24733: clean up the SPIRE containers on a failed initialize, and
tighten the X509 chain assertion
Addresses review on #27429: if a step after the server starts throws,
initialize() now tears down the server container, network and temp socket
directory (and nulls the fields) so a tryInitialize() retry cannot orphan them;
and the X509-SVID test now asserts the returned certificate chain is non-empty
rather than merely non-null.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24733: let the SpiffeSSLContextParameters decorator drive client
auth in the mTLS test, and tighten the deny assertion
Addresses review on #27429: the mTLS test no longer forces
setNeedClientAuth(true) on the server socket, so requiring the client
certificate comes solely from serverParameters.clientAuthentication=REQUIRE
applied by the decorator - the test now actually exercises that the decorator
preserves client authentication (CAMEL-24571). The deny assertion now requires
an SSLException or a (non-timeout, non-connect) SocketException rather than any
IOException. Documented the Linux-Docker-host a [...]
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24733: run the SPIRE integration tests only on Linux
Component ITs run by default on mvn verify, so on Docker Desktop
(macOS/Windows VM) or rootless Podman these would run and fail rather than
skip; @EnabledOnOs(OS.LINUX) skips them off a Linux Docker host, where the host
PID namespace and the bind-mounted Workload API socket work. The
skipITs.ppc64le/s390x properties already cover the non-amd64 CI architectures.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../apache/camel/catalog/test-infra/metadata.json | 11 +
components/camel-spiffe/pom.xml | 13 +
.../spiffe/integration/SpiffeMutualTlsIT.java | 149 ++++++++++
.../spiffe/integration/SpiffeWorkloadApiIT.java | 117 ++++++++
test-infra/camel-test-infra-all/pom.xml | 11 +
.../src/generated/resources/META-INF/metadata.json | 11 +
test-infra/camel-test-infra-spiffe/pom.xml | 71 +++++
.../test/infra/spiffe/common/SpiffeProperties.java | 28 ++
.../infra/spiffe/services/SpiffeInfraService.java | 43 +++
.../services/SpiffeLocalContainerInfraService.java | 310 +++++++++++++++++++++
.../spiffe/services/SpiffeRemoteInfraService.java | 57 ++++
.../test/infra/spiffe/services/SpiffeService.java | 26 ++
.../spiffe/services/SpiffeServiceFactory.java | 79 ++++++
.../infra/spiffe/services/container.properties | 18 ++
.../test/infra/spiffe/services/spire-agent.conf | 38 +++
.../test/infra/spiffe/services/spire-server.conf | 41 +++
test-infra/pom.xml | 1 +
17 files changed, 1024 insertions(+)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/test-infra/metadata.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/test-infra/metadata.json
index 161e007ae3a6..87ae2c087bc9 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/test-infra/metadata.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/test-infra/metadata.json
@@ -680,6 +680,17 @@
"version" : "4.23.0-SNAPSHOT",
"serviceVersion" : "16.3",
"uiSupported" : true
+}, {
+ "service" : "org.apache.camel.test.infra.spiffe.services.SpiffeInfraService",
+ "description" : "SPIFFE/SPIRE server and agent exposing the Workload API",
+ "implementation" :
"org.apache.camel.test.infra.spiffe.services.SpiffeLocalContainerInfraService",
+ "alias" : [ "spiffe" ],
+ "aliasImplementation" : [ ],
+ "groupId" : "org.apache.camel",
+ "artifactId" : "camel-test-infra-spiffe",
+ "version" : "4.23.0-SNAPSHOT",
+ "serviceVersion" : null,
+ "uiSupported" : false
}, {
"service" :
"org.apache.camel.test.infra.weaviate.services.WeaviateInfraService",
"description" : "Weaviate is an open source vector database",
diff --git a/components/camel-spiffe/pom.xml b/components/camel-spiffe/pom.xml
index 068c44928b8c..7beb21d3322d 100644
--- a/components/camel-spiffe/pom.xml
+++ b/components/camel-spiffe/pom.xml
@@ -32,6 +32,13 @@
<name>Camel :: SPIFFE</name>
<description>Camel SPIFFE Workload Identity Component</description>
+ <properties>
+ <!-- the SPIRE server/agent images are published for amd64/arm64 only,
so the SPIRE-backed integration
+ tests are skipped on the other CI architectures -->
+ <skipITs.ppc64le>true</skipITs.ppc64le>
+ <skipITs.s390x>true</skipITs.s390x>
+ </properties>
+
<dependencies>
<dependency>
<groupId>org.apache.camel</groupId>
@@ -55,6 +62,12 @@
<artifactId>camel-test-junit6</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-spiffe</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java
new file mode 100644
index 000000000000..acf5b59849d6
--- /dev/null
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeMutualTlsIT.java
@@ -0,0 +1,149 @@
+/*
+ * 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.integration;
+
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.camel.component.spiffe.SpiffeSSLContextParameters;
+import org.apache.camel.support.jsse.ClientAuthentication;
+import org.apache.camel.support.jsse.SSLContextServerParameters;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Completes a real mutual-TLS handshake through {@link
SpiffeSSLContextParameters} against SVIDs from a live SPIRE
+ * Workload API, asserting that an allow-listed peer connects and a
non-allow-listed one is refused during the
+ * handshake.
+ * <p>
+ * Linux only: the SPIRE agent is run in the host PID namespace with the
Workload API socket bind-mounted, which works
+ * against a Linux Docker daemon but not Docker Desktop's VM or rootless
Podman, so the test is skipped off Linux rather
+ * than left to fail on a developer machine.
+ */
+@EnabledOnOs(OS.LINUX)
+class SpiffeMutualTlsIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service =
SpiffeServiceFactory.createSingletonService();
+
+ private SpiffeSSLContextParameters serverSsl(String acceptedSpiffeIds) {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(acceptedSpiffeIds);
+ SSLContextServerParameters serverParameters = new
SSLContextServerParameters();
+
serverParameters.setClientAuthentication(ClientAuthentication.REQUIRE.name());
+ ssl.setServerParameters(serverParameters);
+ return ssl;
+ }
+
+ private SpiffeSSLContextParameters clientSsl() {
+ SpiffeSSLContextParameters ssl = new SpiffeSSLContextParameters();
+ ssl.setSpiffeSocketPath(service.getWorkloadApiSocketPath());
+ ssl.setAcceptedSpiffeIds(service.getWorkloadSpiffeId());
+ return ssl;
+ }
+
+ @Test
+ void allowsAnAllowListedPeer() throws Exception {
+ SSLContext serverContext =
serverSsl(service.getWorkloadSpiffeId()).createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ assertThatCode(() -> handshake(serverContext,
clientContext)).doesNotThrowAnyException();
+ }
+
+ @Test
+ void refusesANonAllowListedPeer() throws Exception {
+ // the server accepts only an id the client does not have; its SVID is
spiffe://example.org/workload
+ SSLContext serverContext =
serverSsl("spiffe://example.org/not-allowed").createSSLContext(context);
+ SSLContext clientContext = clientSsl().createSSLContext(context);
+
+ // a rejected mutual-TLS handshake surfaces either as a TLS alert
(SSLException) or, on some JSSE stacks, a
+ // connection reset (SocketException) - both mean the peer was refused
mid-handshake. Accept those two, but
+ // exclude a timeout or a failure to connect, which would point to a
broken test rather than a rejected peer.
+ assertThatThrownBy(() -> handshake(serverContext, clientContext))
+ .as("a non-allow-listed peer must be refused during the
handshake, not time out or fail to connect")
+ .isInstanceOfAny(SSLException.class, SocketException.class)
+ .isNotInstanceOf(SocketTimeoutException.class)
+ .isNotInstanceOf(ConnectException.class);
+ }
+
+ /**
+ * Drives a mutual-TLS handshake over a loopback socket: the server
requires a client certificate and both sides
+ * present their SVID. Returns normally when the handshake and a one-byte
exchange complete, and throws when either
+ * side rejects the peer.
+ */
+ private void handshake(SSLContext serverContext, SSLContext clientContext)
throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try (SSLServerSocket serverSocket = (SSLServerSocket)
serverContext.getServerSocketFactory()
+ .createServerSocket(0, 1, InetAddress.getLoopbackAddress())) {
+ // deliberately do NOT force client auth on the socket: requiring
the client certificate must come from the
+ // SpiffeSSLContextParameters serverParameters
(clientAuthentication=REQUIRE) applied by its decorator, so
+ // this test actually covers that the decorator preserves client
authentication (the CAMEL-24571 regression)
+ serverSocket.setSoTimeout(15000);
+
+ Future<Void> server = executor.submit(() -> {
+ try (SSLSocket accepted = (SSLSocket) serverSocket.accept()) {
+ accepted.setSoTimeout(15000);
+ accepted.startHandshake();
+ accepted.getInputStream().read();
+ }
+ return null;
+ });
+
+ int port = serverSocket.getLocalPort();
+ try (SSLSocket clientSocket = (SSLSocket)
clientContext.getSocketFactory()
+ .createSocket(InetAddress.getLoopbackAddress(), port)) {
+ clientSocket.setSoTimeout(15000);
+ clientSocket.startHandshake();
+ clientSocket.getOutputStream().write(42);
+ clientSocket.getOutputStream().flush();
+ }
+ // surface a server-side handshake rejection to the caller as its
underlying cause (an IOException),
+ // rather than the ExecutionException wrapper the Future would
otherwise throw
+ try {
+ server.get(20, TimeUnit.SECONDS);
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof Exception cause) {
+ throw cause;
+ }
+ throw e;
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+}
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
new file mode 100644
index 000000000000..14ba7a5fc5c4
--- /dev/null
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/integration/SpiffeWorkloadApiIT.java
@@ -0,0 +1,117 @@
+/*
+ * 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.integration;
+
+import java.util.List;
+
+import io.spiffe.exception.JwtSvidException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.spiffe.SpiffeConstants;
+import org.apache.camel.test.infra.spiffe.services.SpiffeService;
+import org.apache.camel.test.infra.spiffe.services.SpiffeServiceFactory;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Exercises the camel-spiffe producer against a real SPIRE Workload API:
fetching an X509-SVID and a JWT-SVID, and
+ * validating a JWT-SVID (accepting the configured audience, rejecting
another).
+ * <p>
+ * Linux only: the SPIRE agent is run in the host PID namespace with the
Workload API socket bind-mounted, which works
+ * against a Linux Docker daemon but not Docker Desktop's VM or rootless
Podman, so the test is skipped off Linux rather
+ * than left to fail on a developer machine.
+ */
+@EnabledOnOs(OS.LINUX)
+class SpiffeWorkloadApiIT extends CamelTestSupport {
+
+ @RegisterExtension
+ static SpiffeService service =
SpiffeServiceFactory.createSingletonService();
+
+ private static String socket() {
+ return "RAW(" + service.getWorkloadApiSocketPath() + ")";
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:x509")
+
.toF("spiffe:x509?operation=fetchX509Svid&spiffeSocketPath=%s", socket());
+ from("direct:jwt")
+
.toF("spiffe:jwt?operation=fetchJwtSvid&audience=my-audience&spiffeSocketPath=%s",
socket());
+ from("direct:validateCorrectAudience")
+
.toF("spiffe:valOk?operation=validateJwtSvid&audience=my-audience&spiffeSocketPath=%s",
socket());
+ from("direct:validateWrongAudience")
+
.toF("spiffe:valWrong?operation=validateJwtSvid&audience=other-audience&spiffeSocketPath=%s",
+ socket());
+ }
+ };
+ }
+
+ @Test
+ void fetchesAnX509SvidForThisWorkload() {
+ Exchange out = template.request("direct:x509", e -> {
+ });
+
+ assertThat(out.getException()).isNull();
+ assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID))
+ .isEqualTo(service.getWorkloadSpiffeId());
+ // the default x509Response is the certificate chain, which must carry
at least the leaf certificate
+ assertThat(out.getMessage().getBody(List.class)).isNotEmpty();
+ }
+
+ @Test
+ void fetchesAJwtSvidForThisWorkload() {
+ Exchange out = template.request("direct:jwt", e -> {
+ });
+
+ assertThat(out.getException()).isNull();
+ assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID))
+ .isEqualTo(service.getWorkloadSpiffeId());
+ // a JWT-SVID is a compact JWT: three base64url parts separated by dots
+
assertThat(out.getMessage().getBody(String.class).split("\\.")).hasSize(3);
+ }
+
+ @Test
+ void validatesAJwtSvidAgainstItsAudience() {
+ String token = template.requestBody("direct:jwt", null, String.class);
+
+ Exchange out = template.request("direct:validateCorrectAudience",
+ e -> e.getMessage().setHeader(SpiffeConstants.TOKEN, token));
+
+ assertThat(out.getException()).isNull();
+ assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID))
+ .isEqualTo(service.getWorkloadSpiffeId());
+ }
+
+ @Test
+ void rejectsAJwtSvidMintedForAnotherAudience() {
+ String token = template.requestBody("direct:jwt", null, String.class);
+
+ Exchange out = template.request("direct:validateWrongAudience",
+ e -> e.getMessage().setHeader(SpiffeConstants.TOKEN, token));
+
+ // the token is valid but minted for my-audience, so validation
against other-audience must fail
+ assertThat(out.getException()).isInstanceOf(JwtSvidException.class);
+ }
+}
diff --git a/test-infra/camel-test-infra-all/pom.xml
b/test-infra/camel-test-infra-all/pom.xml
index 2abede7768bf..510f68cbeae0 100644
--- a/test-infra/camel-test-infra-all/pom.xml
+++ b/test-infra/camel-test-infra-all/pom.xml
@@ -316,6 +316,11 @@
<artifactId>camel-test-infra-openfga</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-spiffe</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-infra-pinecone</artifactId>
@@ -661,6 +666,12 @@
<artifactId>camel-test-infra-openfga</artifactId>
</dependency>
</fileSet>
+ <fileSet>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+
<artifactId>camel-test-infra-spiffe</artifactId>
+ </dependency>
+ </fileSet>
<fileSet>
<dependency>
<groupId>org.apache.camel</groupId>
diff --git
a/test-infra/camel-test-infra-all/src/generated/resources/META-INF/metadata.json
b/test-infra/camel-test-infra-all/src/generated/resources/META-INF/metadata.json
index 161e007ae3a6..87ae2c087bc9 100644
---
a/test-infra/camel-test-infra-all/src/generated/resources/META-INF/metadata.json
+++
b/test-infra/camel-test-infra-all/src/generated/resources/META-INF/metadata.json
@@ -680,6 +680,17 @@
"version" : "4.23.0-SNAPSHOT",
"serviceVersion" : "16.3",
"uiSupported" : true
+}, {
+ "service" : "org.apache.camel.test.infra.spiffe.services.SpiffeInfraService",
+ "description" : "SPIFFE/SPIRE server and agent exposing the Workload API",
+ "implementation" :
"org.apache.camel.test.infra.spiffe.services.SpiffeLocalContainerInfraService",
+ "alias" : [ "spiffe" ],
+ "aliasImplementation" : [ ],
+ "groupId" : "org.apache.camel",
+ "artifactId" : "camel-test-infra-spiffe",
+ "version" : "4.23.0-SNAPSHOT",
+ "serviceVersion" : null,
+ "uiSupported" : false
}, {
"service" :
"org.apache.camel.test.infra.weaviate.services.WeaviateInfraService",
"description" : "Weaviate is an open source vector database",
diff --git a/test-infra/camel-test-infra-spiffe/pom.xml
b/test-infra/camel-test-infra-spiffe/pom.xml
new file mode 100644
index 000000000000..1d7f16447ce8
--- /dev/null
+++ b/test-infra/camel-test-infra-spiffe/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>camel-test-infra-parent</artifactId>
+ <groupId>org.apache.camel</groupId>
+ <relativePath>../camel-test-infra-parent/pom.xml</relativePath>
+ <version>4.23.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>camel-test-infra-spiffe</artifactId>
+ <name>Camel :: Test Infra :: SPIFFE</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <profiles>
+ <profile>
+ <id>spiffe-it-test</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ <property>
+ <name>spiffe-it-test</name>
+ </property>
+ </activation>
+ <properties>
+ <skipITs>false</skipITs>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/common/SpiffeProperties.java
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/common/SpiffeProperties.java
new file mode 100644
index 000000000000..ee843653f677
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/common/SpiffeProperties.java
@@ -0,0 +1,28 @@
+/*
+ * 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.test.infra.spiffe.common;
+
+public final class SpiffeProperties {
+ public static final String SPIFFE_SERVER_CONTAINER =
"spiffe.server.container";
+ public static final String SPIFFE_AGENT_CONTAINER =
"spiffe.agent.container";
+ public static final String SPIFFE_SOCKET_PATH = "spiffe.socket.path";
+ public static final String SPIFFE_TRUST_DOMAIN = "spiffe.trust.domain";
+ public static final String SPIFFE_WORKLOAD_ID = "spiffe.workload.id";
+
+ private SpiffeProperties() {
+ }
+}
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeInfraService.java
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeInfraService.java
new file mode 100644
index 000000000000..dd39cca14fb4
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeInfraService.java
@@ -0,0 +1,43 @@
+/*
+ * 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.test.infra.spiffe.services;
+
+import org.apache.camel.test.infra.common.services.InfrastructureService;
+
+/**
+ * Test infra service for SPIFFE/SPIRE: a SPIRE server plus an agent exposing
the Workload API, so a test can exercise
+ * camel-spiffe against a real Workload API endpoint rather than a mock.
+ */
+public interface SpiffeInfraService extends InfrastructureService {
+
+ /**
+ * The Workload API endpoint, as a {@code unix://} URI, for the
camel-spiffe {@code spiffeSocketPath} option (or the
+ * {@code SPIFFE_ENDPOINT_SOCKET} environment variable).
+ */
+ String getWorkloadApiSocketPath();
+
+ /**
+ * The trust domain the SPIRE server was configured with (for example
{@code example.org}).
+ */
+ String getTrustDomain();
+
+ /**
+ * The SPIFFE ID of the workload SVID the agent issues to the test process
(for example
+ * {@code spiffe://example.org/workload}).
+ */
+ String getWorkloadSpiffeId();
+}
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java
new file mode 100644
index 000000000000..20aaecb545af
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeLocalContainerInfraService.java
@@ -0,0 +1,310 @@
+/*
+ * 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.test.infra.spiffe.services;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+
+import com.sun.security.auth.module.UnixSystem;
+import org.apache.camel.spi.annotations.InfraService;
+import org.apache.camel.test.infra.common.LocalPropertyResolver;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.SelinuxContext;
+import
org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+/**
+ * Runs a SPIRE server and agent so a test can fetch SVIDs from a real
Workload API.
+ * <p>
+ * The agent's Workload API Unix socket is bind-mounted to a short host path
(the {@code AF_UNIX} {@code sun_path} limit
+ * is ~108 bytes, so a socket under a deep temp tree would fail to connect
from the host). The agent runs in the host
+ * PID namespace because the {@code unix} workload attestor resolves the
calling process through {@code /proc}, which a
+ * container cannot see otherwise; a single registration entry is created for
{@code unix:uid:<the test process uid>} so
+ * the test JVM is issued {@code spiffe://<trustDomain>/workload}.
+ * <p>
+ * This assumes a Linux Docker host: the host PID namespace, the Unix-socket
bind mount and the {@code unix:uid}
+ * selector all rely on the test JVM and the containers sharing one Linux
kernel. It is not expected to work on Docker
+ * Desktop (macOS/Windows, where the daemon runs in a separate VM) or on
rootless Podman (uid remapping) - which is also
+ * why the component gates these integration tests off non-amd64 CI. It
targets a Linux CI/development Docker.
+ */
+@InfraService(service = SpiffeInfraService.class,
+ description = "SPIFFE/SPIRE server and agent exposing the
Workload API",
+ serviceAlias = { "spiffe" })
+public class SpiffeLocalContainerInfraService implements SpiffeInfraService,
ContainerService<GenericContainer<?>> {
+
+ public static final String TRUST_DOMAIN = "example.org";
+ public static final String AGENT_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN +
"/agent";
+ public static final String WORKLOAD_SPIFFE_ID = "spiffe://" + TRUST_DOMAIN
+ "/workload";
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SpiffeLocalContainerInfraService.class);
+
+ private static final String SERVER_ALIAS = "spire-server";
+ private static final int SERVER_PORT = 8081;
+ private static final String SERVER_BIN = "/opt/spire/bin/spire-server";
+ private static final String SERVER_CONF =
"/opt/spire/conf/server/server.conf";
+ private static final String AGENT_CONF =
"/opt/spire/conf/agent/agent.conf";
+ private static final String SOCKET_CONTAINER_DIR =
"/tmp/spire-agent/public";
+ private static final String SOCKET_FILE = "api.sock";
+ private static final String SERVER_CONF_RESOURCE =
"org/apache/camel/test/infra/spiffe/services/spire-server.conf";
+ private static final String AGENT_CONF_RESOURCE =
"org/apache/camel/test/infra/spiffe/services/spire-agent.conf";
+
+ private final String serverImage;
+ private final String agentImage;
+
+ private Network network;
+ private GenericContainer<?> server;
+ private GenericContainer<?> agent;
+ private Path hostSocketDir;
+ private String socketPath;
+
+ public SpiffeLocalContainerInfraService() {
+
this(LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_SERVER_CONTAINER),
+
LocalPropertyResolver.getProperty(SpiffeLocalContainerInfraService.class,
+ SpiffeProperties.SPIFFE_AGENT_CONTAINER));
+ }
+
+ public SpiffeLocalContainerInfraService(String serverImage, String
agentImage) {
+ this.serverImage = serverImage;
+ this.agentImage = agentImage;
+ }
+
+ @Override
+ public void initialize() {
+ try {
+ doInitialize();
+ } catch (RuntimeException e) {
+ // a partial start would leak the server container, the network
and the temp socket directory, and a
+ // TestServiceUtil.tryInitialize() retry would overwrite the
fields and orphan them; tear down whatever
+ // came up before propagating
+ cleanup();
+ throw e;
+ }
+ }
+
+ private void doInitialize() {
+ createHostSocketDir();
+ network = Network.newNetwork();
+
+ // the image entrypoint is "spire-server run", so only -config is
passed; the config is copied in (no bind mount
+ // needed for it) and port 8081 is exposed purely so the
listening-port wait can tell when the API is up
+ server = new GenericContainer<>(serverImage)
+ .withNetwork(network)
+ .withNetworkAliases(SERVER_ALIAS)
+
.withCopyFileToContainer(MountableFile.forClasspathResource(SERVER_CONF_RESOURCE),
SERVER_CONF)
+ .withExposedPorts(SERVER_PORT)
+ .withCommand("-config", SERVER_CONF)
+ .waitingFor(Wait.forListeningPort());
+ LOG.info("Starting the SPIRE server container");
+ server.start();
+ waitForServerHealthy();
+
+ String joinToken = generateJoinToken();
+ createWorkloadEntry();
+
+ // the agent needs the host PID namespace so the unix workload
attestor can resolve the host test process, and
+ // the socket directory bind-mounted (SELinux-shared) so the host can
reach the Workload API socket it creates
+ agent = new GenericContainer<>(agentImage)
+ .withNetwork(network)
+
.withCopyFileToContainer(MountableFile.forClasspathResource(AGENT_CONF_RESOURCE),
AGENT_CONF)
+ .withCreateContainerCmdModifier(cmd ->
cmd.getHostConfig().withPidMode("host"))
+ .withStartupCheckStrategy(new IsRunningStartupCheckStrategy())
+ .withCommand("-config", AGENT_CONF, "-joinToken", joinToken);
+ agent.addFileSystemBind(hostSocketDir.toString(),
SOCKET_CONTAINER_DIR, BindMode.READ_WRITE, SelinuxContext.SHARED);
+ LOG.info("Starting the SPIRE agent container");
+ agent.start();
+ waitForSocket();
+
+ socketPath = "unix://" + hostSocketDir.resolve(SOCKET_FILE);
+ registerProperties();
+ LOG.info("SPIRE Workload API available at {}", socketPath);
+ }
+
+ private void createHostSocketDir() {
+ try {
+ // keep the socket path short (AF_UNIX sun_path is limited to ~108
bytes); /tmp keeps it well within that
+ hostSocketDir = Files.createTempDirectory(Path.of("/tmp"),
"spiffe");
+ // world-accessible so both sides of the bind mount can use the
socket: the agent container (running as
+ // root, which creates the socket) and the host test process may
run under different uids, so an
+ // owner-only directory would stop one of them traversing it to
create or connect to the socket
+ hostSocketDir.toFile().setReadable(true, false);
+ hostSocketDir.toFile().setWritable(true, false);
+ hostSocketDir.toFile().setExecutable(true, false);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not create the SPIFFE Workload
API socket directory", e);
+ }
+ }
+
+ private void waitForServerHealthy() {
+ long deadline = System.currentTimeMillis() +
Duration.ofSeconds(30).toMillis();
+ RuntimeException last = null;
+ while (System.currentTimeMillis() < deadline) {
+ try {
+ Container.ExecResult result =
server.execInContainer(SERVER_BIN, "healthcheck");
+ if (result.getExitCode() == 0) {
+ return;
+ }
+ last = new RuntimeException("healthcheck exit " +
result.getExitCode() + ": " + result.getStderr());
+ } catch (IOException e) {
+ last = new RuntimeException(e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ sleep(500);
+ }
+ throw new RuntimeException("SPIRE server did not become healthy in
time", last);
+ }
+
+ private String generateJoinToken() {
+ Container.ExecResult result = exec(server, SERVER_BIN, "token",
"generate", "-spiffeID", AGENT_SPIFFE_ID,
+ "-ttl", "3600");
+ for (String line : result.getStdout().split("\\R")) {
+ if (line.startsWith("Token: ")) {
+ return line.substring("Token: ".length()).trim();
+ }
+ }
+ throw new RuntimeException("Could not parse a join token from: " +
result.getStdout());
+ }
+
+ private void createWorkloadEntry() {
+ long uid = new UnixSystem().getUid();
+ exec(server, SERVER_BIN, "entry", "create", "-parentID",
AGENT_SPIFFE_ID, "-spiffeID", WORKLOAD_SPIFFE_ID,
+ "-selector", "unix:uid:" + uid);
+ }
+
+ private void waitForSocket() {
+ Path socket = hostSocketDir.resolve(SOCKET_FILE);
+ long deadline = System.currentTimeMillis() +
Duration.ofSeconds(60).toMillis();
+ while (System.currentTimeMillis() < deadline) {
+ if (Files.exists(socket)) {
+ return;
+ }
+ sleep(250);
+ }
+ throw new RuntimeException("The SPIRE agent did not create the
Workload API socket at " + socket);
+ }
+
+ private Container.ExecResult exec(GenericContainer<?> container, String...
command) {
+ try {
+ Container.ExecResult result = container.execInContainer(command);
+ if (result.getExitCode() != 0) {
+ throw new RuntimeException(
+ "Command " + String.join(" ", command) + " failed
(exit "
+ + result.getExitCode() + "): " +
result.getStderr());
+ }
+ return result;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void sleep(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void registerProperties() {
+ System.setProperty(SpiffeProperties.SPIFFE_SOCKET_PATH, socketPath);
+ System.setProperty(SpiffeProperties.SPIFFE_TRUST_DOMAIN, TRUST_DOMAIN);
+ System.setProperty(SpiffeProperties.SPIFFE_WORKLOAD_ID,
WORKLOAD_SPIFFE_ID);
+ }
+
+ @Override
+ public void shutdown() {
+ LOG.info("Stopping the SPIRE containers");
+ cleanup();
+ }
+
+ // stops whatever has been started and nulls the fields, so it is safe to
call from a failed initialize() (before
+ // a retry) as well as from shutdown(); every step is best-effort so one
failure does not leak the rest
+ private void cleanup() {
+ agent = stopQuietly(agent);
+ server = stopQuietly(server);
+ if (network != null) {
+ try {
+ network.close();
+ } catch (RuntimeException e) {
+ // best effort on cleanup
+ }
+ network = null;
+ }
+ if (hostSocketDir != null) {
+ deleteQuietly(hostSocketDir.resolve(SOCKET_FILE));
+ deleteQuietly(hostSocketDir);
+ hostSocketDir = null;
+ }
+ socketPath = null;
+ }
+
+ private static GenericContainer<?> stopQuietly(GenericContainer<?>
container) {
+ if (container != null) {
+ try {
+ container.stop();
+ } catch (RuntimeException e) {
+ // best effort on cleanup
+ }
+ }
+ return null;
+ }
+
+ private static void deleteQuietly(Path path) {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ // best effort on cleanup
+ }
+ }
+
+ @Override
+ public GenericContainer<?> getContainer() {
+ return agent;
+ }
+
+ @Override
+ public String getWorkloadApiSocketPath() {
+ return socketPath;
+ }
+
+ @Override
+ public String getTrustDomain() {
+ return TRUST_DOMAIN;
+ }
+
+ @Override
+ public String getWorkloadSpiffeId() {
+ return WORKLOAD_SPIFFE_ID;
+ }
+}
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeRemoteInfraService.java
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeRemoteInfraService.java
new file mode 100644
index 000000000000..2d747e571eec
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeRemoteInfraService.java
@@ -0,0 +1,57 @@
+/*
+ * 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.test.infra.spiffe.services;
+
+import org.apache.camel.test.infra.spiffe.common.SpiffeProperties;
+
+/**
+ * A SPIFFE service backed by an already-running SPIRE Workload API,
configured through system properties.
+ */
+public class SpiffeRemoteInfraService implements SpiffeInfraService {
+
+ @Override
+ public void registerProperties() {
+ // NO-OP
+ }
+
+ @Override
+ public void initialize() {
+ registerProperties();
+ }
+
+ @Override
+ public void shutdown() {
+ // NO-OP
+ }
+
+ @Override
+ public String getWorkloadApiSocketPath() {
+ return System.getProperty(SpiffeProperties.SPIFFE_SOCKET_PATH);
+ }
+
+ @Override
+ public String getTrustDomain() {
+ String trustDomain =
System.getProperty(SpiffeProperties.SPIFFE_TRUST_DOMAIN);
+ return trustDomain != null ? trustDomain :
SpiffeLocalContainerInfraService.TRUST_DOMAIN;
+ }
+
+ @Override
+ public String getWorkloadSpiffeId() {
+ String workloadId =
System.getProperty(SpiffeProperties.SPIFFE_WORKLOAD_ID);
+ return workloadId != null ? workloadId :
SpiffeLocalContainerInfraService.WORKLOAD_SPIFFE_ID;
+ }
+}
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeService.java
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeService.java
new file mode 100644
index 000000000000..1dee3489f460
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeService.java
@@ -0,0 +1,26 @@
+/*
+ * 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.test.infra.spiffe.services;
+
+import org.apache.camel.test.infra.common.services.ContainerTestService;
+import org.apache.camel.test.infra.common.services.TestService;
+
+/**
+ * Test infra service for SPIFFE/SPIRE
+ */
+public interface SpiffeService extends SpiffeInfraService, TestService,
ContainerTestService {
+}
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeServiceFactory.java
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeServiceFactory.java
new file mode 100644
index 000000000000..c68792681dc0
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/java/org/apache/camel/test/infra/spiffe/services/SpiffeServiceFactory.java
@@ -0,0 +1,79 @@
+/*
+ * 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.test.infra.spiffe.services;
+
+import org.apache.camel.test.infra.common.services.SimpleTestServiceBuilder;
+import org.apache.camel.test.infra.common.services.SingletonService;
+
+public final class SpiffeServiceFactory {
+
+ private SpiffeServiceFactory() {
+ }
+
+ private static class SingletonSpiffeService extends
SingletonService<SpiffeService> implements SpiffeService {
+ public SingletonSpiffeService(SpiffeService service, String name) {
+ super(service, name);
+ }
+
+ @Override
+ public String getWorkloadApiSocketPath() {
+ return getService().getWorkloadApiSocketPath();
+ }
+
+ @Override
+ public String getTrustDomain() {
+ return getService().getTrustDomain();
+ }
+
+ @Override
+ public String getWorkloadSpiffeId() {
+ return getService().getWorkloadSpiffeId();
+ }
+ }
+
+ public static SimpleTestServiceBuilder<SpiffeService> builder() {
+ return new SimpleTestServiceBuilder<>("spiffe");
+ }
+
+ public static SpiffeService createService() {
+ return builder()
+ .addLocalMapping(SpiffeLocalContainerTestService::new)
+ .addRemoteMapping(SpiffeRemoteTestService::new)
+ .build();
+ }
+
+ public static SpiffeService createSingletonService() {
+ return SingletonServiceHolder.INSTANCE;
+ }
+
+ private static class SingletonServiceHolder {
+ static final SpiffeService INSTANCE;
+ static {
+ SimpleTestServiceBuilder<SpiffeService> instance = builder();
+ instance.addLocalMapping(
+ () -> new SingletonSpiffeService(new
SpiffeLocalContainerTestService(), "spiffe"))
+ .addRemoteMapping(SpiffeRemoteTestService::new);
+ INSTANCE = instance.build();
+ }
+ }
+
+ public static class SpiffeLocalContainerTestService extends
SpiffeLocalContainerInfraService implements SpiffeService {
+ }
+
+ public static class SpiffeRemoteTestService extends
SpiffeRemoteInfraService implements SpiffeService {
+ }
+}
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/container.properties
b/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/container.properties
new file mode 100644
index 000000000000..fedc50102b24
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/container.properties
@@ -0,0 +1,18 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+spiffe.server.container=ghcr.io/spiffe/spire-server:1.15.3
+spiffe.agent.container=ghcr.io/spiffe/spire-agent:1.15.3
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-agent.conf
b/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-agent.conf
new file mode 100644
index 000000000000..bf8d174f3394
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-agent.conf
@@ -0,0 +1,38 @@
+#
+# 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.
+#
+
+agent {
+ data_dir = "/tmp/spire-agent/data"
+ log_level = "INFO"
+ trust_domain = "example.org"
+ server_address = "spire-server"
+ server_port = "8081"
+ socket_path = "/tmp/spire-agent/public/api.sock"
+ insecure_bootstrap = true
+}
+
+plugins {
+ KeyManager "memory" {
+ plugin_data {}
+ }
+ NodeAttestor "join_token" {
+ plugin_data {}
+ }
+ WorkloadAttestor "unix" {
+ plugin_data {}
+ }
+}
diff --git
a/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-server.conf
b/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-server.conf
new file mode 100644
index 000000000000..8d81e179deae
--- /dev/null
+++
b/test-infra/camel-test-infra-spiffe/src/main/resources/org/apache/camel/test/infra/spiffe/services/spire-server.conf
@@ -0,0 +1,41 @@
+#
+# 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.
+#
+
+server {
+ bind_address = "0.0.0.0"
+ bind_port = "8081"
+ trust_domain = "example.org"
+ data_dir = "/tmp/spire-server/data"
+ log_level = "INFO"
+ ca_ttl = "24h"
+ default_x509_svid_ttl = "1h"
+}
+
+plugins {
+ DataStore "sql" {
+ plugin_data {
+ database_type = "sqlite3"
+ connection_string = "/tmp/spire-server/data/datastore.sqlite3"
+ }
+ }
+ KeyManager "memory" {
+ plugin_data {}
+ }
+ NodeAttestor "join_token" {
+ plugin_data {}
+ }
+}
diff --git a/test-infra/pom.xml b/test-infra/pom.xml
index 3b4de073da2b..1141f9d2cc65 100644
--- a/test-infra/pom.xml
+++ b/test-infra/pom.xml
@@ -67,6 +67,7 @@
<module>camel-test-infra-nats</module>
<module>camel-test-infra-opa</module>
<module>camel-test-infra-openfga</module>
+ <module>camel-test-infra-spiffe</module>
<module>camel-test-infra-pinecone</module>
<module>camel-test-infra-pulsar</module>
<module>camel-test-infra-redis</module>