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 bcf9481495b4 CAMEL-25369: camel-spiffe - fix the SpiffeSecurityPolicy 
doc example, and add the negative tests (#27419)
bcf9481495b4 is described below

commit bcf9481495b4fb76ccd0d35bf29d8e1401e2a468
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Oct 6 11:23:04 2026 +0200

    CAMEL-25369: camel-spiffe - fix the SpiffeSecurityPolicy doc example, and 
add the negative tests (#27419)
    
    * CAMEL-25369: camel-spiffe - fix the SpiffeSecurityPolicy doc example, and 
add the negative tests
    
    Follow-up to CAMEL-24735 (#27385) from davsclaus's review. The code is 
correct; the documentation
    shipped with it is not.
    
    The documented route cannot work. The example configured no client 
authentication, and
    SSLContextServerParameters.clientAuthentication defaults to null, so the 
JSSE default applies and
    the server never requests a client certificate. 
SSLSession.getPeerCertificates() then throws, the
    peer identity is null, and the policy denies - correctly, failing closed, 
but that means the
    documented route rejects every request. The example now builds the server
    SpiffeSSLContextParameters with clientAuthentication=REQUIRE.
    
    Two further caveats a reader could not have inferred are now stated in an 
IMPORTANT block:
    
    - the server must use SpiffeSSLContextParameters, not a plain truststore 
that happens to trust the
      SPIFFE CA - such a truststore accepts any SVID that CA issued, which is 
weaker than the allow-list
      this policy appears to give you;
    - only camel-netty and camel-netty-http put the SSLSession on the message 
today, so behind
      platform-http, jetty, undertow or servlet the policy has no verified 
certificate and denies
      everything.
    
    The catalog copy of the component doc is a verbatim mirror and is updated 
in step; both files are
    byte-identical after the change.
    
    Five negative tests, all of which the current code already passes - they 
pin behaviour rather than
    fix it: a String-valued CamelNettySSLSession header (a sender cannot assert 
an identity, because an
    SSLSession does not convert from a String), the two near-miss ids
    (spiffe://example.org.evil/frontend and spiffe://example.org/frontend/x), a 
malformed spiffe:// URI
    SAN, and a leaf certificate that is not an X509Certificate.
    
    Mutating isAuthorized from an exact Set.contains to a prefix match fails
    deniesAnIdWhosePathOnlyStartsWithTheAllowedOne and nothing else, so that 
test is not vacuous. The
    trust-domain near-miss is not covered by that mutation; it rests on the 
exact-match semantics.
    
    davsclaus also asked whether sslSessionHeader should be required to start 
with "Camel". It should
    not, and the String-header test is the demonstration: an SSLSession is a 
Java object, so it cannot
    arrive through an HTTP string header at all.
    
    Module suite 46 tests green.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-25369: note that the TLS-level allow-list gates every 
SpiffeSecurityPolicy on a shared server
    
    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 5 <[email protected]>
---
 .../camel/catalog/docs/spiffe-component.adoc       | 29 +++++++++
 .../src/main/docs/spiffe-component.adoc            | 29 +++++++++
 .../component/spiffe/SpiffeSecurityPolicyTest.java | 70 ++++++++++++++++++++++
 3 files changed, 128 insertions(+)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
index 9fdd30362d45..dcb978c9e29a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spiffe-component.adoc
@@ -155,6 +155,14 @@ verified SPIFFE ID:
 
 [source,java]
 ------------------------------------------------------------
+// the server must REQUIRE a client certificate, or the policy has no verified 
peer to authorize
+SpiffeSSLContextParameters serverSsl = new SpiffeSSLContextParameters();
+serverSsl.setAcceptedSpiffeIds("spiffe://example.org/frontend");
+SSLContextServerParameters serverParameters = new SSLContextServerParameters();
+serverParameters.setClientAuthentication(ClientAuthentication.REQUIRE.name());
+serverSsl.setServerParameters(serverParameters);
+getCamelContext().getRegistry().bind("spiffeSsl", serverSsl);
+
 SpiffeSecurityPolicy spiffePolicy = new 
SpiffeSecurityPolicy("spiffe://example.org/frontend");
 getCamelContext().getRegistry().bind("spiffePolicy", spiffePolicy);
 
@@ -163,6 +171,27 @@ 
from("netty-http:https://0.0.0.0:8443?sslContextParameters=#spiffeSsl";)
     .to("direct:handleOrder");
 ------------------------------------------------------------
 
+[IMPORTANT]
+====
+A few things about the server side decide whether, and for which peers, this 
policy can work.
+
+* *The server must request a client certificate.* 
`serverParameters.clientAuthentication` defaults to unset, which
+  leaves the JSSE default: the server never asks. 
`SSLSession.getPeerCertificates()` then throws and the policy
+  fails closed, so *every* request is denied. Set it to `REQUIRE`, as above.
+* *The server must use `SpiffeSSLContextParameters`*, not a plain truststore 
that happens to trust the SPIFFE CA.
+  Such a truststore accepts any SVID that CA issued, which is a weaker check 
than the allow-list this policy
+  appears to give you.
+* *The handshake allow-list gates every policy.* 
`SpiffeSSLContextParameters.acceptedSpiffeIds` decides who may
+  connect at all, so a peer it rejects never reaches any route's policy. When 
several routes with different
+  `SpiffeSecurityPolicy` allow-lists share one server, this TLS-level list 
must contain every ID those policies
+  accept (or use `acceptAnySpiffeId` and let each policy narrow it), otherwise 
the handshake turns a peer away
+  before its route could have authorized it.
+
+Only camel-netty and camel-netty-http put the `SSLSession` on the message 
today, as the `CamelNettySSLSession`
+header. Behind platform-http, jetty, undertow or servlet there is no session 
on the exchange, so the policy has no
+verified peer certificate to read and denies everything.
+====
+
 The peer SPIFFE ID is taken from the *verified* TLS peer certificate the 
consumer put on the exchange - the
 `CamelNettySSLSession` header for camel-netty-http - and never from a message 
header a sender could set (see the
 Security notes below). A peer that is not on the `acceptedSpiffeIds` 
allow-list, or that presents no verified SVID at
diff --git a/components/camel-spiffe/src/main/docs/spiffe-component.adoc 
b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
index 9fdd30362d45..dcb978c9e29a 100644
--- a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
+++ b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
@@ -155,6 +155,14 @@ verified SPIFFE ID:
 
 [source,java]
 ------------------------------------------------------------
+// the server must REQUIRE a client certificate, or the policy has no verified 
peer to authorize
+SpiffeSSLContextParameters serverSsl = new SpiffeSSLContextParameters();
+serverSsl.setAcceptedSpiffeIds("spiffe://example.org/frontend");
+SSLContextServerParameters serverParameters = new SSLContextServerParameters();
+serverParameters.setClientAuthentication(ClientAuthentication.REQUIRE.name());
+serverSsl.setServerParameters(serverParameters);
+getCamelContext().getRegistry().bind("spiffeSsl", serverSsl);
+
 SpiffeSecurityPolicy spiffePolicy = new 
SpiffeSecurityPolicy("spiffe://example.org/frontend");
 getCamelContext().getRegistry().bind("spiffePolicy", spiffePolicy);
 
@@ -163,6 +171,27 @@ 
from("netty-http:https://0.0.0.0:8443?sslContextParameters=#spiffeSsl";)
     .to("direct:handleOrder");
 ------------------------------------------------------------
 
+[IMPORTANT]
+====
+A few things about the server side decide whether, and for which peers, this 
policy can work.
+
+* *The server must request a client certificate.* 
`serverParameters.clientAuthentication` defaults to unset, which
+  leaves the JSSE default: the server never asks. 
`SSLSession.getPeerCertificates()` then throws and the policy
+  fails closed, so *every* request is denied. Set it to `REQUIRE`, as above.
+* *The server must use `SpiffeSSLContextParameters`*, not a plain truststore 
that happens to trust the SPIFFE CA.
+  Such a truststore accepts any SVID that CA issued, which is a weaker check 
than the allow-list this policy
+  appears to give you.
+* *The handshake allow-list gates every policy.* 
`SpiffeSSLContextParameters.acceptedSpiffeIds` decides who may
+  connect at all, so a peer it rejects never reaches any route's policy. When 
several routes with different
+  `SpiffeSecurityPolicy` allow-lists share one server, this TLS-level list 
must contain every ID those policies
+  accept (or use `acceptAnySpiffeId` and let each policy narrow it), otherwise 
the handshake turns a peer away
+  before its route could have authorized it.
+
+Only camel-netty and camel-netty-http put the `SSLSession` on the message 
today, as the `CamelNettySSLSession`
+header. Behind platform-http, jetty, undertow or servlet there is no session 
on the exchange, so the policy has no
+verified peer certificate to read and denies everything.
+====
+
 The peer SPIFFE ID is taken from the *verified* TLS peer certificate the 
consumer put on the exchange - the
 `CamelNettySSLSession` header for camel-netty-http - and never from a message 
header a sender could set (see the
 Security notes below). A peer that is not on the `acceptedSpiffeIds` 
allow-list, or that presents no verified SVID at
diff --git 
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicyTest.java
 
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicyTest.java
index f61605cdf1a6..d45a7ed177d2 100644
--- 
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicyTest.java
+++ 
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeSecurityPolicyTest.java
@@ -173,4 +173,74 @@ class SpiffeSecurityPolicyTest extends CamelTestSupport {
                 .hasMessageContaining("No verified SPIFFE peer identity");
         authorized.assertIsSatisfied();
     }
+
+    /**
+     * A sender cannot assert an identity by setting the session header: an 
{@link SSLSession} is a Java object, so a
+     * String header does not convert to one and the policy sees no session at 
all.
+     */
+    @Test
+    void deniesAStringValuedSslSessionHeader() throws Exception {
+        MockEndpoint authorized = getMockEndpoint("mock:authorized");
+        authorized.expectedMessageCount(0);
+
+        Exchange out = template.request("direct:start", e -> {
+            
e.getMessage().setHeader(SpiffeSecurityPolicy.DEFAULT_SSL_SESSION_HEADER, 
FRONTEND);
+            e.getMessage().setBody("payload");
+        });
+
+        
assertThat(out.getException()).isInstanceOf(CamelAuthorizationException.class)
+                .hasMessageContaining("No verified SPIFFE peer identity");
+        assertThat(out.getProperty(SpiffeConstants.PEER_SPIFFE_ID)).isNull();
+        authorized.assertIsSatisfied();
+    }
+
+    @Test
+    void deniesAnIdWhoseTrustDomainOnlyLooksLikeTheAllowedOne() throws 
Exception {
+        MockEndpoint authorized = getMockEndpoint("mock:authorized");
+        authorized.expectedMessageCount(0);
+
+        Exchange out = 
sendWithSession(sessionPresenting(certWithUriSans("spiffe://example.org.evil/frontend")));
+
+        
assertThat(out.getException()).isInstanceOf(CamelAuthorizationException.class);
+        assertThat(out.getProperty(SpiffeConstants.PEER_SPIFFE_ID)).isNull();
+        authorized.assertIsSatisfied();
+    }
+
+    @Test
+    void deniesAnIdWhosePathOnlyStartsWithTheAllowedOne() throws Exception {
+        MockEndpoint authorized = getMockEndpoint("mock:authorized");
+        authorized.expectedMessageCount(0);
+
+        Exchange out = 
sendWithSession(sessionPresenting(certWithUriSans(FRONTEND + "/x")));
+
+        
assertThat(out.getException()).isInstanceOf(CamelAuthorizationException.class);
+        assertThat(out.getProperty(SpiffeConstants.PEER_SPIFFE_ID)).isNull();
+        authorized.assertIsSatisfied();
+    }
+
+    @Test
+    void deniesAMalformedSpiffeUriSan() throws Exception {
+        MockEndpoint authorized = getMockEndpoint("mock:authorized");
+        authorized.expectedMessageCount(0);
+
+        Exchange out = 
sendWithSession(sessionPresenting(certWithUriSans("spiffe://")));
+
+        
assertThat(out.getException()).isInstanceOf(CamelAuthorizationException.class)
+                .hasMessageContaining("No verified SPIFFE peer identity");
+        assertThat(out.getProperty(SpiffeConstants.PEER_SPIFFE_ID)).isNull();
+        authorized.assertIsSatisfied();
+    }
+
+    @Test
+    void deniesALeafCertificateThatIsNotAnX509() throws Exception {
+        MockEndpoint authorized = getMockEndpoint("mock:authorized");
+        authorized.expectedMessageCount(0);
+
+        Exchange out = 
sendWithSession(sessionPresenting(mock(Certificate.class)));
+
+        
assertThat(out.getException()).isInstanceOf(CamelAuthorizationException.class)
+                .hasMessageContaining("No verified SPIFFE peer identity");
+        assertThat(out.getProperty(SpiffeConstants.PEER_SPIFFE_ID)).isNull();
+        authorized.assertIsSatisfied();
+    }
 }

Reply via email to