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 91a779233283 CAMEL-24739: camel-spiffe - fetchX509Svid keeps the
private key off the message by default (#27053)
91a779233283 is described below
commit 91a779233283bcd47568dd0953f2fe130d826e93
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Sep 30 19:04:22 2026 +0200
CAMEL-24739: camel-spiffe - fetchX509Svid keeps the private key off the
message by default (#27053)
fetchX509Svid set the whole X509Svid as the message body, including its
private key, so any route that only needed its own identity exposed key
material to tracing, logging and error handlers.
A new producer option x509Response controls what fetchX509Svid puts on the
body. The default returns the certificate chain only, keeping the private key
off the message; the full SVID is still available by explicit opt-in.
Closes #27053
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../apache/camel/catalog/components/spiffe.json | 8 ++-
.../camel/catalog/docs/spiffe-component.adoc | 12 ++--
.../spiffe/SpiffeComponentConfigurer.java | 6 ++
.../component/spiffe/SpiffeEndpointConfigurer.java | 6 ++
.../component/spiffe/SpiffeEndpointUriFactory.java | 3 +-
.../org/apache/camel/component/spiffe/spiffe.json | 8 ++-
.../src/main/docs/spiffe-component.adoc | 12 ++--
.../component/spiffe/SpiffeConfiguration.java | 19 ++++++
.../camel/component/spiffe/SpiffeConstants.java | 5 +-
.../camel/component/spiffe/SpiffeProducer.java | 13 ++++-
.../camel/component/spiffe/SpiffeX509Response.java | 44 ++++++++++++++
.../camel/component/spiffe/SpiffeProducerTest.java | 67 ++++++++++++++++++++--
.../dsl/SpiffeComponentBuilderFactory.java | 25 ++++++++
.../endpoint/dsl/SpiffeEndpointBuilderFactory.java | 49 +++++++++++++++-
14 files changed, 255 insertions(+), 22 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
index 7487f79aa19a..fafd50e4986a 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spiffe.json
@@ -31,14 +31,15 @@
"autowiredEnabled": { "index": 4, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
"workloadApiClient": { "index": 5, "kind": "property", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "description": "An existing
WorkloadApiClient to use. When set, [...]
"allowOperationHeader": { "index": 6, "kind": "property", "displayName":
"Allow Operation Header", "group": "security", "label": "security", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "security": "insecure:dev",
"defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header may
[...]
- "spiffeSocketPath": { "index": 7, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock} [...]
+ "spiffeSocketPath": { "index": 7, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock} [...]
+ "x509Response": { "index": 8, "kind": "property", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": " [...]
},
"headers": {
"CamelSpiffeOperation": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"org.apache.camel.component.spiffe.SpiffeOperation or String", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "Overrides the operation to be used by the producer. Ignored
unless the endpoint sets allowOperationHeader=true, because the operation
decides whether the endpoint validates a token [...]
"CamelSpiffeAudience": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The comma-separated audience(s) for the
fetchJwtSvid operation. Ignored by validateJwtSvid, which always validates
against the configured audience: there the audience is the check that binds the
token to this workload, not a parameter [...]
"CamelSpiffeToken": { "index": 2, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The JWT-SVID token to validate, for the
validateJwtSvid operation.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#TOKEN" },
"CamelSpiffeSpiffeId": { "index": 3, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The SPIFFE ID of the returned SVID.",
"constantName": "org.apache.camel.component.spiffe.SpiffeConstants#SPIFFE_ID" },
- "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The expiry of the returned JWT-SVID.",
"constantName": "org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
+ "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The expiry of the returned SVID: the
token expiry for fetchJwtSvid, or the leaf certificate's notAfter for
fetchX509Svid.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
},
"properties": {
"label": { "index": 0, "kind": "path", "displayName": "Label", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Logical name of the endpoint" },
@@ -47,6 +48,7 @@
"lazyStartProducer": { "index": 3, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
"workloadApiClient": { "index": 4, "kind": "parameter", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "description": "An existing
WorkloadApiClient to use. When set [...]
"allowOperationHeader": { "index": 5, "kind": "parameter", "displayName":
"Allow Operation Header", "group": "security", "label": "security", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "security": "insecure:dev",
"defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header ma
[...]
- "spiffeSocketPath": { "index": 6, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock [...]
+ "spiffeSocketPath": { "index": 6, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock [...]
+ "x509Response": { "index": 7, "kind": "parameter", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": [...]
}
}
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 c9c1930d1ca5..ae86c550660e 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
@@ -57,9 +57,12 @@ For advanced scenarios an already-configured
`io.spiffe.workloadapi.WorkloadApiC
The component supports the following producer operations:
-* `fetchX509Svid` — fetches the default X.509-SVID from the Workload API. The
message body is set to the
-`io.spiffe.svid.x509svid.X509Svid` (certificate chain, private key and SPIFFE
ID) and the `CamelSpiffeSpiffeId`
-header to its SPIFFE ID.
+* `fetchX509Svid` — fetches the default X.509-SVID from the Workload API. By
default (`x509Response=chain`) the message
+body is set to the certificate chain only (a
`List<java.security.cert.X509Certificate>`, without the private key), so a
+route never handles key material unless it asks for it. Set
`x509Response=svid` to get the whole
+`io.spiffe.svid.x509svid.X509Svid` including the private key (needed for
programmatic mTLS), or `x509Response=id` to
+leave the body untouched. In every case the `CamelSpiffeSpiffeId` and
`CamelSpiffeExpiry` (the leaf certificate's
+expiry) headers carry the identity.
* `fetchJwtSvid` — fetches a JWT-SVID for the configured `audience` (or the
`CamelSpiffeAudience` header). The
message body is set to the JWT token string, with the `CamelSpiffeSpiffeId`
and `CamelSpiffeExpiry` headers.
* `validateJwtSvid` — validates a JWT-SVID against the `audience`. The token
is taken from the first of:
@@ -77,7 +80,8 @@ The `fetchX509Svid` and `fetchJwtSvid` operations place
sensitive key material o
carries the workload's private key, and the JWT-SVID is a bearer token. Route
authors are trusted with Exchange
contents, but you should avoid logging or tracing the message body for these
operations — for example via the
`log`/`trace` components or the message-history / breadcrumb EIPs — to prevent
accidental disclosure of the key
-or token.
+or token. `fetchX509Svid` only puts the private key on the body when
`x509Response=svid` is set explicitly; the
+default (`chain`) and `id` keep the key off the message entirely.
====
== Example
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
index c5f39f05820e..c2122aaa502a 100644
---
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeComponentConfigurer.java
@@ -43,6 +43,8 @@ public class SpiffeComponentConfigurer extends
PropertyConfigurerSupport impleme
case "spiffeSocketPath":
getOrCreateConfiguration(target).setSpiffeSocketPath(property(camelContext,
java.lang.String.class, value)); return true;
case "workloadapiclient":
case "workloadApiClient":
getOrCreateConfiguration(target).setWorkloadApiClient(property(camelContext,
io.spiffe.workloadapi.WorkloadApiClient.class, value)); return true;
+ case "x509response":
+ case "x509Response":
getOrCreateConfiguration(target).setX509Response(property(camelContext,
org.apache.camel.component.spiffe.SpiffeX509Response.class, value)); return
true;
default: return false;
}
}
@@ -68,6 +70,8 @@ public class SpiffeComponentConfigurer extends
PropertyConfigurerSupport impleme
case "spiffeSocketPath": return java.lang.String.class;
case "workloadapiclient":
case "workloadApiClient": return
io.spiffe.workloadapi.WorkloadApiClient.class;
+ case "x509response":
+ case "x509Response": return
org.apache.camel.component.spiffe.SpiffeX509Response.class;
default: return null;
}
}
@@ -89,6 +93,8 @@ public class SpiffeComponentConfigurer extends
PropertyConfigurerSupport impleme
case "spiffeSocketPath": return
getOrCreateConfiguration(target).getSpiffeSocketPath();
case "workloadapiclient":
case "workloadApiClient": return
getOrCreateConfiguration(target).getWorkloadApiClient();
+ case "x509response":
+ case "x509Response": return
getOrCreateConfiguration(target).getX509Response();
default: return null;
}
}
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
index 0eedcc77671a..e7d3a7764205 100644
---
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointConfigurer.java
@@ -33,6 +33,8 @@ public class SpiffeEndpointConfigurer extends
PropertyConfigurerSupport implemen
case "spiffeSocketPath":
target.getConfiguration().setSpiffeSocketPath(property(camelContext,
java.lang.String.class, value)); return true;
case "workloadapiclient":
case "workloadApiClient":
target.getConfiguration().setWorkloadApiClient(property(camelContext,
io.spiffe.workloadapi.WorkloadApiClient.class, value)); return true;
+ case "x509response":
+ case "x509Response":
target.getConfiguration().setX509Response(property(camelContext,
org.apache.camel.component.spiffe.SpiffeX509Response.class, value)); return
true;
default: return false;
}
}
@@ -55,6 +57,8 @@ public class SpiffeEndpointConfigurer extends
PropertyConfigurerSupport implemen
case "spiffeSocketPath": return java.lang.String.class;
case "workloadapiclient":
case "workloadApiClient": return
io.spiffe.workloadapi.WorkloadApiClient.class;
+ case "x509response":
+ case "x509Response": return
org.apache.camel.component.spiffe.SpiffeX509Response.class;
default: return null;
}
}
@@ -73,6 +77,8 @@ public class SpiffeEndpointConfigurer extends
PropertyConfigurerSupport implemen
case "spiffeSocketPath": return
target.getConfiguration().getSpiffeSocketPath();
case "workloadapiclient":
case "workloadApiClient": return
target.getConfiguration().getWorkloadApiClient();
+ case "x509response":
+ case "x509Response": return
target.getConfiguration().getX509Response();
default: return null;
}
}
diff --git
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
index 38ae3005bca9..810c90fcf042 100644
---
a/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
+++
b/components/camel-spiffe/src/generated/java/org/apache/camel/component/spiffe/SpiffeEndpointUriFactory.java
@@ -24,7 +24,7 @@ public class SpiffeEndpointUriFactory extends
org.apache.camel.support.component
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(7);
+ Set<String> props = new HashSet<>(8);
props.add("allowOperationHeader");
props.add("audience");
props.add("label");
@@ -32,6 +32,7 @@ public class SpiffeEndpointUriFactory extends
org.apache.camel.support.component
props.add("operation");
props.add("spiffeSocketPath");
props.add("workloadApiClient");
+ props.add("x509Response");
PROPERTY_NAMES = Collections.unmodifiableSet(props);
SECRET_PROPERTY_NAMES = Collections.emptySet();
ENDPOINT_IDENTITY_PROPERTY_NAMES = Collections.emptySet();
diff --git
a/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
b/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
index 7487f79aa19a..fafd50e4986a 100644
---
a/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
+++
b/components/camel-spiffe/src/generated/resources/META-INF/org/apache/camel/component/spiffe/spiffe.json
@@ -31,14 +31,15 @@
"autowiredEnabled": { "index": 4, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
"workloadApiClient": { "index": 5, "kind": "property", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "description": "An existing
WorkloadApiClient to use. When set, [...]
"allowOperationHeader": { "index": 6, "kind": "property", "displayName":
"Allow Operation Header", "group": "security", "label": "security", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "security": "insecure:dev",
"defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header may
[...]
- "spiffeSocketPath": { "index": 7, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock} [...]
+ "spiffeSocketPath": { "index": 7, "kind": "property", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock} [...]
+ "x509Response": { "index": 8, "kind": "property", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": " [...]
},
"headers": {
"CamelSpiffeOperation": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"org.apache.camel.component.spiffe.SpiffeOperation or String", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "Overrides the operation to be used by the producer. Ignored
unless the endpoint sets allowOperationHeader=true, because the operation
decides whether the endpoint validates a token [...]
"CamelSpiffeAudience": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The comma-separated audience(s) for the
fetchJwtSvid operation. Ignored by validateJwtSvid, which always validates
against the configured audience: there the audience is the check that binds the
token to this workload, not a parameter [...]
"CamelSpiffeToken": { "index": 2, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The JWT-SVID token to validate, for the
validateJwtSvid operation.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#TOKEN" },
"CamelSpiffeSpiffeId": { "index": 3, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The SPIFFE ID of the returned SVID.",
"constantName": "org.apache.camel.component.spiffe.SpiffeConstants#SPIFFE_ID" },
- "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The expiry of the returned JWT-SVID.",
"constantName": "org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
+ "CamelSpiffeExpiry": { "index": 4, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"java.util.Date", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The expiry of the returned SVID: the
token expiry for fetchJwtSvid, or the leaf certificate's notAfter for
fetchX509Svid.", "constantName":
"org.apache.camel.component.spiffe.SpiffeConstants#EXPIRY" }
},
"properties": {
"label": { "index": 0, "kind": "path", "displayName": "Label", "group":
"producer", "label": "", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Logical name of the endpoint" },
@@ -47,6 +48,7 @@
"lazyStartProducer": { "index": 3, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
"workloadApiClient": { "index": 4, "kind": "parameter", "displayName":
"Workload Api Client", "group": "advanced", "label": "advanced", "required":
false, "type": "object", "javaType": "io.spiffe.workloadapi.WorkloadApiClient",
"deprecated": false, "deprecationNote": "", "autowired": true, "secret": false,
"configurationClass": "org.apache.camel.component.spiffe.SpiffeConfiguration",
"configurationField": "configuration", "description": "An existing
WorkloadApiClient to use. When set [...]
"allowOperationHeader": { "index": 5, "kind": "parameter", "displayName":
"Allow Operation Header", "group": "security", "label": "security", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "security": "insecure:dev",
"defaultValue": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "Whether the CamelSpiffeOperation header ma
[...]
- "spiffeSocketPath": { "index": 6, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock [...]
+ "spiffeSocketPath": { "index": 6, "kind": "parameter", "displayName":
"Spiffe Socket Path", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": "The address of the SPIFFE Workload API
endpoint (for example {code unix:\/\/\/tmp\/agent.sock [...]
+ "x509Response": { "index": 7, "kind": "parameter", "displayName": "X509
Response", "group": "security", "label": "producer,security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.spiffe.SpiffeX509Response", "enum": [ "svid",
"chain", "id" ], "deprecated": false, "autowired": false, "secret": false,
"defaultValue": "chain", "configurationClass":
"org.apache.camel.component.spiffe.SpiffeConfiguration", "configurationField":
"configuration", "description": [...]
}
}
diff --git a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
index c9c1930d1ca5..ae86c550660e 100644
--- a/components/camel-spiffe/src/main/docs/spiffe-component.adoc
+++ b/components/camel-spiffe/src/main/docs/spiffe-component.adoc
@@ -57,9 +57,12 @@ For advanced scenarios an already-configured
`io.spiffe.workloadapi.WorkloadApiC
The component supports the following producer operations:
-* `fetchX509Svid` — fetches the default X.509-SVID from the Workload API. The
message body is set to the
-`io.spiffe.svid.x509svid.X509Svid` (certificate chain, private key and SPIFFE
ID) and the `CamelSpiffeSpiffeId`
-header to its SPIFFE ID.
+* `fetchX509Svid` — fetches the default X.509-SVID from the Workload API. By
default (`x509Response=chain`) the message
+body is set to the certificate chain only (a
`List<java.security.cert.X509Certificate>`, without the private key), so a
+route never handles key material unless it asks for it. Set
`x509Response=svid` to get the whole
+`io.spiffe.svid.x509svid.X509Svid` including the private key (needed for
programmatic mTLS), or `x509Response=id` to
+leave the body untouched. In every case the `CamelSpiffeSpiffeId` and
`CamelSpiffeExpiry` (the leaf certificate's
+expiry) headers carry the identity.
* `fetchJwtSvid` — fetches a JWT-SVID for the configured `audience` (or the
`CamelSpiffeAudience` header). The
message body is set to the JWT token string, with the `CamelSpiffeSpiffeId`
and `CamelSpiffeExpiry` headers.
* `validateJwtSvid` — validates a JWT-SVID against the `audience`. The token
is taken from the first of:
@@ -77,7 +80,8 @@ The `fetchX509Svid` and `fetchJwtSvid` operations place
sensitive key material o
carries the workload's private key, and the JWT-SVID is a bearer token. Route
authors are trusted with Exchange
contents, but you should avoid logging or tracing the message body for these
operations — for example via the
`log`/`trace` components or the message-history / breadcrumb EIPs — to prevent
accidental disclosure of the key
-or token.
+or token. `fetchX509Svid` only puts the private key on the body when
`x509Response=svid` is set explicitly; the
+default (`chain`) and `id` keep the key off the message entirely.
====
== Example
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
index 92d6ca7d3370..98fc0e053286 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConfiguration.java
@@ -28,6 +28,9 @@ public class SpiffeConfiguration implements Cloneable {
@UriParam(defaultValue = "fetchX509Svid")
private SpiffeOperation operation = SpiffeOperation.fetchX509Svid;
+ @UriParam(label = "producer,security", defaultValue = "chain")
+ private SpiffeX509Response x509Response = SpiffeX509Response.chain;
+
@UriParam(label = "security")
private String spiffeSocketPath;
@@ -54,6 +57,22 @@ public class SpiffeConfiguration implements Cloneable {
this.operation = operation;
}
+ public SpiffeX509Response getX509Response() {
+ return x509Response;
+ }
+
+ /**
+ * What the {@code fetchX509Svid} operation returns in the message body.
+ * <p/>
+ * Defaults to {@code chain}: the X.509 certificate chain without the
private key, so a route never handles key
+ * material unless it asks for it. Choose {@code svid} to get the whole
{@code X509Svid} including the <em>private
+ * key</em> (needed for programmatic mTLS), or {@code id} to leave the
body untouched. The SPIFFE ID and expiry are
+ * exposed through the {@code CamelSpiffeSpiffeId} and {@code
CamelSpiffeExpiry} headers in every case.
+ */
+ public void setX509Response(SpiffeX509Response x509Response) {
+ this.x509Response = x509Response;
+ }
+
/**
* The address of the SPIFFE Workload API endpoint (for example {@code
unix:///tmp/agent.sock} or
* {@code tcp://127.0.0.1:8082}). When not set, the {@code
SPIFFE_ENDPOINT_SOCKET} environment variable is used.
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
index 349e30523515..996c472c36ef 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeConstants.java
@@ -42,7 +42,10 @@ public final class SpiffeConstants {
@Metadata(label = "producer", description = "The SPIFFE ID of the returned
SVID.", javaType = "String")
public static final String SPIFFE_ID = HEADER_PREFIX + "SpiffeId";
- @Metadata(label = "producer", description = "The expiry of the returned
JWT-SVID.", javaType = "java.util.Date")
+ @Metadata(label = "producer",
+ description = "The expiry of the returned SVID: the token expiry
for fetchJwtSvid, or the leaf"
+ + " certificate's notAfter for fetchX509Svid.",
+ javaType = "java.util.Date")
public static final String EXPIRY = HEADER_PREFIX + "Expiry";
private SpiffeConstants() {
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
index e068eceac645..25ecd8632109 100644
---
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeProducer.java
@@ -52,8 +52,19 @@ public class SpiffeProducer extends DefaultProducer {
private void fetchX509Svid(WorkloadApiClient client, Exchange exchange)
throws Exception {
X509Svid svid = client.fetchX509Context().getDefaultSvid();
Message message = getMessageForResponse(exchange);
- message.setBody(svid);
+ // the identity is always available through the headers, so a route
that only needs it can avoid the key
message.setHeader(SpiffeConstants.SPIFFE_ID,
svid.getSpiffeId().toString());
+ message.setHeader(SpiffeConstants.EXPIRY,
svid.getLeaf().getNotAfter());
+ switch (getEndpoint().getConfiguration().getX509Response()) {
+ // the full SVID carries the private key; the chain does not; id
leaves the body untouched
+ case svid -> message.setBody(svid);
+ case chain -> message.setBody(svid.getChain());
+ case id -> {
+ // leave the body untouched: the identity is exposed through
the headers only
+ }
+ default -> throw new IllegalArgumentException(
+ "Unsupported x509Response: " +
getEndpoint().getConfiguration().getX509Response());
+ }
}
private void fetchJwtSvid(WorkloadApiClient client, Exchange exchange)
throws Exception {
diff --git
a/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeX509Response.java
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeX509Response.java
new file mode 100644
index 000000000000..139645547090
--- /dev/null
+++
b/components/camel-spiffe/src/main/java/org/apache/camel/component/spiffe/SpiffeX509Response.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * What the {@code fetchX509Svid} operation puts in the message body.
+ * <p/>
+ * The SPIFFE ID and expiry are always exposed as the {@code
CamelSpiffeSpiffeId} and {@code CamelSpiffeExpiry} headers,
+ * so a route that only needs its identity can use the default {@link #chain}
(or {@link #id}) and never handle the
+ * private key.
+ */
+public enum SpiffeX509Response {
+
+ /**
+ * The full {@link io.spiffe.svid.x509svid.X509Svid}, which carries the
private key. Needed for programmatic mTLS;
+ * opt in to it rather than getting the key by default.
+ */
+ svid,
+
+ /**
+ * The X.509 certificate chain only ({@code
List<java.security.cert.X509Certificate>}), without the private key.
+ * This is the default.
+ */
+ chain,
+
+ /**
+ * Leaves the message body untouched; the identity is available only
through the headers.
+ */
+ id
+}
diff --git
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
index d0367b598e75..6fc5e99ee5c1 100644
---
a/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
+++
b/components/camel-spiffe/src/test/java/org/apache/camel/component/spiffe/SpiffeProducerTest.java
@@ -16,7 +16,9 @@
*/
package org.apache.camel.component.spiffe;
+import java.security.cert.X509Certificate;
import java.util.Date;
+import java.util.List;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.svid.jwtsvid.JwtSvid;
@@ -43,20 +45,77 @@ class SpiffeProducerTest extends CamelTestSupport {
return spiffeId;
}
- @Test
- void fetchX509Svid() throws Exception {
- SpiffeId id = spiffeId("spiffe://example.org/workload");
+ private final Date notAfter = new Date();
+ private final List<X509Certificate> chain =
List.of(mock(X509Certificate.class));
+
+ private X509Svid mockX509Svid(String id) throws Exception {
+ // build the nested mocks first: stubbing one inside another when(...)
call trips Mockito
+ SpiffeId sid = spiffeId(id);
+ X509Certificate leaf = mock(X509Certificate.class);
+ when(leaf.getNotAfter()).thenReturn(notAfter);
X509Svid svid = mock(X509Svid.class);
- when(svid.getSpiffeId()).thenReturn(id);
+ when(svid.getSpiffeId()).thenReturn(sid);
+ when(svid.getChain()).thenReturn(chain);
+ when(svid.getLeaf()).thenReturn(leaf);
X509Context ctx = mock(X509Context.class);
when(ctx.getDefaultSvid()).thenReturn(svid);
when(client.fetchX509Context()).thenReturn(ctx);
+ return svid;
+ }
+
+ @Test
+ void fetchX509Svid() throws Exception {
+ mockX509Svid("spiffe://example.org/workload");
Exchange out =
template.request("spiffe:test?workloadApiClient=#client&operation=fetchX509Svid",
e -> {
});
+ // default response is the certificate chain: no private key on the
body unless x509Response=svid is set
+ assertThat(out.getMessage().getBody()).isSameAs(chain);
+
assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isEqualTo("spiffe://example.org/workload");
+
assertThat(out.getMessage().getHeader(SpiffeConstants.EXPIRY)).isEqualTo(notAfter);
+ }
+
+ @Test
+ void fetchX509SvidSvidReturnsTheFullSvid() throws Exception {
+ X509Svid svid = mockX509Svid("spiffe://example.org/workload");
+
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=fetchX509Svid&x509Response=svid",
e -> {
+ });
+
+ // opt-in: the whole SVID, which carries the private key
assertThat(out.getMessage().getBody()).isSameAs(svid);
assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isEqualTo("spiffe://example.org/workload");
+
assertThat(out.getMessage().getHeader(SpiffeConstants.EXPIRY)).isEqualTo(notAfter);
+ }
+
+ @Test
+ void fetchX509SvidChainOmitsThePrivateKey() throws Exception {
+ mockX509Svid("spiffe://example.org/workload");
+
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=fetchX509Svid&x509Response=chain",
e -> {
+ });
+
+ // the certificate chain, not the SVID: no private key in the body
+ assertThat(out.getMessage().getBody()).isSameAs(chain);
+
assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isEqualTo("spiffe://example.org/workload");
+
assertThat(out.getMessage().getHeader(SpiffeConstants.EXPIRY)).isEqualTo(notAfter);
+ }
+
+ @Test
+ void fetchX509SvidIdLeavesTheBodyUntouched() throws Exception {
+ mockX509Svid("spiffe://example.org/workload");
+
+ Exchange out = template.request(
+
"spiffe:test?workloadApiClient=#client&operation=fetchX509Svid&x509Response=id",
+ e -> e.getIn().setBody("original-body"));
+
+ // body untouched: the identity is exposed only through the headers,
so no key material is handled
+ assertThat(out.getMessage().getBody()).isEqualTo("original-body");
+
assertThat(out.getMessage().getHeader(SpiffeConstants.SPIFFE_ID)).isEqualTo("spiffe://example.org/workload");
+
assertThat(out.getMessage().getHeader(SpiffeConstants.EXPIRY)).isEqualTo(notAfter);
}
@Test
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
index b9495f540e09..82ebb9499999 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SpiffeComponentBuilderFactory.java
@@ -208,6 +208,30 @@ public interface SpiffeComponentBuilderFactory {
doSetProperty("spiffeSocketPath", spiffeSocketPath);
return this;
}
+
+
+ /**
+ * What the fetchX509Svid operation returns in the message body.
+ * Defaults to chain: the X.509 certificate chain without the private
+ * key, so a route never handles key material unless it asks for it.
+ * Choose svid to get the whole X509Svid including the private key
+ * (needed for programmatic mTLS), or id to leave the body untouched.
+ * The SPIFFE ID and expiry are exposed through the CamelSpiffeSpiffeId
+ * and CamelSpiffeExpiry headers in every case.
+ *
+ * The option is a:
+ *
<code>org.apache.camel.component.spiffe.SpiffeX509Response</code>
type.
+ *
+ * Default: chain
+ * Group: security
+ *
+ * @param x509Response the value to set
+ * @return the dsl builder
+ */
+ default SpiffeComponentBuilder
x509Response(org.apache.camel.component.spiffe.SpiffeX509Response x509Response)
{
+ doSetProperty("x509Response", x509Response);
+ return this;
+ }
}
class SpiffeComponentBuilderImpl
@@ -237,6 +261,7 @@ public interface SpiffeComponentBuilderFactory {
case "workloadApiClient":
getOrCreateConfiguration((SpiffeComponent)
component).setWorkloadApiClient((io.spiffe.workloadapi.WorkloadApiClient)
value); return true;
case "allowOperationHeader":
getOrCreateConfiguration((SpiffeComponent)
component).setAllowOperationHeader((boolean) value); return true;
case "spiffeSocketPath":
getOrCreateConfiguration((SpiffeComponent)
component).setSpiffeSocketPath((java.lang.String) value); return true;
+ case "x509Response": getOrCreateConfiguration((SpiffeComponent)
component).setX509Response((org.apache.camel.component.spiffe.SpiffeX509Response)
value); return true;
default: return false;
}
}
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
index f48784f220d9..5c6df09c0512 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpiffeEndpointBuilderFactory.java
@@ -150,6 +150,52 @@ public interface SpiffeEndpointBuilderFactory {
doSetProperty("spiffeSocketPath", spiffeSocketPath);
return this;
}
+ /**
+ * What the fetchX509Svid operation returns in the message body.
+ * Defaults to chain: the X.509 certificate chain without the private
+ * key, so a route never handles key material unless it asks for it.
+ * Choose svid to get the whole X509Svid including the private key
+ * (needed for programmatic mTLS), or id to leave the body untouched.
+ * The SPIFFE ID and expiry are exposed through the CamelSpiffeSpiffeId
+ * and CamelSpiffeExpiry headers in every case.
+ *
+ * The option is a:
+ * <code>org.apache.camel.component.spiffe.SpiffeX509Response</code>
+ * type.
+ *
+ * Default: chain
+ * Group: security
+ *
+ * @param x509Response the value to set
+ * @return the dsl builder
+ */
+ default SpiffeEndpointBuilder
x509Response(org.apache.camel.component.spiffe.SpiffeX509Response x509Response)
{
+ doSetProperty("x509Response", x509Response);
+ return this;
+ }
+ /**
+ * What the fetchX509Svid operation returns in the message body.
+ * Defaults to chain: the X.509 certificate chain without the private
+ * key, so a route never handles key material unless it asks for it.
+ * Choose svid to get the whole X509Svid including the private key
+ * (needed for programmatic mTLS), or id to leave the body untouched.
+ * The SPIFFE ID and expiry are exposed through the CamelSpiffeSpiffeId
+ * and CamelSpiffeExpiry headers in every case.
+ *
+ * The option will be converted to a
+ * <code>org.apache.camel.component.spiffe.SpiffeX509Response</code>
+ * type.
+ *
+ * Default: chain
+ * Group: security
+ *
+ * @param x509Response the value to set
+ * @return the dsl builder
+ */
+ default SpiffeEndpointBuilder x509Response(String x509Response) {
+ doSetProperty("x509Response", x509Response);
+ return this;
+ }
}
/**
@@ -366,7 +412,8 @@ public interface SpiffeEndpointBuilderFactory {
return "CamelSpiffeSpiffeId";
}
/**
- * The expiry of the returned JWT-SVID.
+ * The expiry of the returned SVID: the token expiry for fetchJwtSvid,
+ * or the leaf certificate's notAfter for fetchX509Svid.
*
* The option is a: {@code java.util.Date} type.
*