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>

Reply via email to