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();
+ }
}