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 641dae5e058d CAMEL-25552: camel-hashicorp-vault - the properties
function no longer shares the secret engine between lookups (#27679)
641dae5e058d is described below
commit 641dae5e058d12666b38882a52f5b07d8f207b07
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 14:25:20 2026 +0200
CAMEL-25552: camel-hashicorp-vault - the properties function no longer
shares the secret engine between lookups (#27679)
* CAMEL-25552: camel-hashicorp-vault - the properties function no longer
shares the secret engine between lookups
The engine of a hashicorp: lookup was kept in a field of the shared
properties function, so concurrent lookups could read from the wrong engine and
a lookup without engine used the engine of the lookup before. The engine is now
local to each lookup and defaults to the 'secret' engine. A missing secret with
a field now resolves to no value (or the default value) instead of failing with
a NullPointerException. Fixed the docs example of a default value with a
version, which was missing t [...]
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-25552: camel-hashicorp-vault - update a test comment after rebase
on CAMEL-25518
A lookup without engine now reads from the default secret engine instead of
a null engine.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-25552: camel-hashicorp-vault - a response without data resolves to
no value also without a field
Address review: the lookup without a field read
rawSecret.getData().get("data") unguarded, so a response without data failed
with a NullPointerException. Both paths now read the secret data once,
null-safe.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-25552: Address review feedback
Move the upgrade guide note from 4.23 to 4.24 (this is not in 4.23.0) and
mention that a version without engine is now read as a version, say that a
default value with a field needs no engine, and use # for the field in the
javadoc.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../catalog/docs/hashicorp-vault-component.adoc | 12 +-
.../src/main/docs/hashicorp-vault-component.adoc | 12 +-
.../vault/HashicorpVaultPropertiesFunction.java | 50 ++++--
.../HashicorpVaultPropertiesFunctionTest.java | 184 +++++++++++++++++++++
.../vault/HashicorpVaultReloadTriggerTaskTest.java | 2 +-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 29 ++++
.../modules/ROOT/pages/camel-4x-upgrade-guide.adoc | 1 +
7 files changed, 265 insertions(+), 25 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
index d8b2e2ad3091..845a2c662568 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
@@ -363,6 +363,12 @@ YAML::
Where route will be the name of the secret stored in the HashiCorp Vault
instance, in the 'secret' engine.
+NOTE: The engine is the part before the first `:`; for a property with a
field, only the part before the `#` counts.
+When the property has no engine, such as `{{hashicorp:route}}` or
`{{hashicorp:database#username}}`, the secret is read
+from the 'secret' engine. Without a field, a default value needs the engine,
as in `{{hashicorp:secret:route:default}}`:
+in `{{hashicorp:route:default}}` the engine would be `route`. With a field it
does not, as in
+`{{hashicorp:database#username:admin}}`.
+
You could specify a default value in case the secret is not present on
HashiCorp Vault instance:
[tabs]
@@ -540,7 +546,7 @@ Java::
[source,java]
----
from("direct:start")
- .to("{{hashicorp:route:default@2}}");
+ .to("{{hashicorp:secret:route:default@2}}");
----
XML::
@@ -550,7 +556,7 @@ XML::
<camelContext>
<route>
<from uri="direct:start"/>
- <to uri="{{hashicorp:route:default@2}}"/>
+ <to uri="{{hashicorp:secret:route:default@2}}"/>
</route>
</camelContext>
----
@@ -564,7 +570,7 @@ YAML::
uri: direct:start
steps:
- to:
- uri: "{{hashicorp:route:default@2}}"
+ uri: "{{hashicorp:secret:route:default@2}}"
----
====
diff --git
a/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc
b/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc
index d8b2e2ad3091..845a2c662568 100644
---
a/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc
+++
b/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc
@@ -363,6 +363,12 @@ YAML::
Where route will be the name of the secret stored in the HashiCorp Vault
instance, in the 'secret' engine.
+NOTE: The engine is the part before the first `:`; for a property with a
field, only the part before the `#` counts.
+When the property has no engine, such as `{{hashicorp:route}}` or
`{{hashicorp:database#username}}`, the secret is read
+from the 'secret' engine. Without a field, a default value needs the engine,
as in `{{hashicorp:secret:route:default}}`:
+in `{{hashicorp:route:default}}` the engine would be `route`. With a field it
does not, as in
+`{{hashicorp:database#username:admin}}`.
+
You could specify a default value in case the secret is not present on
HashiCorp Vault instance:
[tabs]
@@ -540,7 +546,7 @@ Java::
[source,java]
----
from("direct:start")
- .to("{{hashicorp:route:default@2}}");
+ .to("{{hashicorp:secret:route:default@2}}");
----
XML::
@@ -550,7 +556,7 @@ XML::
<camelContext>
<route>
<from uri="direct:start"/>
- <to uri="{{hashicorp:route:default@2}}"/>
+ <to uri="{{hashicorp:secret:route:default@2}}"/>
</route>
</camelContext>
----
@@ -564,7 +570,7 @@ YAML::
uri: direct:start
steps:
- to:
- uri: "{{hashicorp:route:default@2}}"
+ uri: "{{hashicorp:secret:route:default@2}}"
----
====
diff --git
a/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
index a1909d805768..04082f515402 100644
---
a/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
+++
b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
@@ -62,13 +62,14 @@ import org.springframework.vault.support.VaultResponse;
* This implementation is to return the secret value associated with a key.
The properties related to this kind of
* Properties Function are all prefixed with <tt>hashicorp:</tt>. For example
asking for
* <tt>hashicorp:engine:token</tt>, will return the secret value associated to
the secret named token on Hashicorp Vault
- * instance on the engine 'engine'.
+ * instance on the engine 'engine'. When no engine is given, such as
<tt>hashicorp:token</tt>, the 'secret' engine is
+ * used.
*
- * Another way of retrieving a secret value is using the following notation
<tt>hashicorp:engine:database/username</tt>:
+ * Another way of retrieving a secret value is using the following notation
<tt>hashicorp:engine:database#username</tt>:
* in this case the field username of the secret database will be returned. As
a fallback, the user could provide a
* default value, which will be returned in case the secret doesn't exist, the
secret has been marked for deletion or,
* for example, if a particular field of the secret doesn't exist. For using
this feature, the user could use the
- * following notation <tt>hashicorp:engine:database/username:admin</tt>. The
admin value will be returned as default
+ * following notation <tt>hashicorp:engine:database#username:admin</tt>. The
admin value will be returned as default
* value, if the conditions above were all met.
*/
@@ -87,12 +88,12 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
= "CAMEL_HASHICORP_VAULT_CLOUD";
private static final String CAMEL_HASHICORP_VAULT_NAMESPACE_ENV
= "CAMEL_HASHICORP_VAULT_NAMESPACE";
+ private static final String DEFAULT_ENGINE = "secret";
private CamelContext camelContext;
private VaultTemplate client;
private final Set<String> secrets = ConcurrentHashMap.newKeySet();
private final Map<String, Integer> secretVersions = new
ConcurrentHashMap<>();
- private String engine;
private String namespace;
private boolean cloud;
@@ -159,6 +160,8 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
@Override
public String apply(String remainder) {
+ // the function is shared by all lookups (also concurrent ones), so
the parsed parts must be local
+ String engine = DEFAULT_ENGINE;
String key = remainder;
String subkey = null;
String returnValue = null;
@@ -166,8 +169,12 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
String version = null;
if (remainder.contains("#")) {
String keyRemainder = StringHelper.before(remainder, "#");
- engine = StringHelper.before(keyRemainder, ":");
- key = StringHelper.after(keyRemainder, ":");
+ if (keyRemainder.contains(":")) {
+ engine = StringHelper.before(keyRemainder, ":");
+ key = StringHelper.after(keyRemainder, ":");
+ } else {
+ key = keyRemainder;
+ }
subkey = StringHelper.after(remainder, "#");
defaultValue = StringHelper.after(subkey, ":");
if (ObjectHelper.isNotEmpty(defaultValue)) {
@@ -207,11 +214,14 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
key = StringHelper.before(key, "@");
}
}
+ } else if (remainder.contains("@")) {
+ version = StringHelper.after(remainder, "@");
+ key = StringHelper.before(remainder, "@");
}
if (key != null) {
try {
- returnValue = getSecretFromSource(key, subkey, defaultValue,
version);
+ returnValue = getSecretFromSource(engine, key, subkey,
defaultValue, version);
} catch (Exception e) {
throw new RuntimeCamelException(
"Error getting secret from vault using key: " + key +
" due to: " + e.getMessage(), e);
@@ -221,7 +231,7 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
return returnValue;
}
- private String getSecretFromSource(String key, String subkey, String
defaultValue, String version) {
+ private String getSecretFromSource(String engine, String key, String
subkey, String defaultValue, String version) {
// capture name of secret in the form the refresh task reads the
metadata of
String secretName = trackedSecretName(engine, key);
secrets.add(secretName);
@@ -240,18 +250,22 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
completePath = completePath + "?version=" + version;
}
VaultResponse rawSecret = client.read(completePath);
- if (ObjectHelper.isNotEmpty(rawSecret)) {
- returnValue = rawSecret.getData().get("data").toString();
+ // the secret may not exist, or the response may have no data:
then there is no value (nor field)
+ Map<String, Object> responseData = rawSecret != null ?
rawSecret.getData() : null;
+ Object secretData = responseData != null ?
responseData.get("data") : null;
+ if (secretData != null) {
+ returnValue = secretData.toString();
if (ObjectHelper.isEmpty(version)) {
- captureVersion(secretName, rawSecret);
+ captureVersion(secretName, responseData);
}
}
if (ObjectHelper.isNotEmpty(subkey)) {
- Object field = ((Map)
rawSecret.getData().get("data")).get(subkey);
- if (ObjectHelper.isNotEmpty(field)) {
- returnValue = field.toString();
- } else {
- returnValue = null;
+ returnValue = null;
+ if (secretData instanceof Map<?, ?> data) {
+ Object field = data.get(subkey);
+ if (ObjectHelper.isNotEmpty(field)) {
+ returnValue = field.toString();
+ }
}
}
if (ObjectHelper.isEmpty(returnValue)) {
@@ -276,8 +290,8 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
return secretEngine == null || "secret".equals(secretEngine) ? key :
secretEngine + ":" + key;
}
- private void captureVersion(String secretName, VaultResponse rawSecret) {
- if (rawSecret.getData().get("metadata") instanceof Map<?, ?> metadata
&& metadata.get("version") != null) {
+ private void captureVersion(String secretName, Map<String, Object>
responseData) {
+ if (responseData.get("metadata") instanceof Map<?, ?> metadata &&
metadata.get("version") != null) {
try {
// keep the oldest version resolved: a value resolved earlier
may still be in use, so the refresh
// task must compare with that one (at worst one extra reload)
diff --git
a/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunctionTest.java
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunctionTest.java
new file mode 100644
index 000000000000..7e1efd682734
--- /dev/null
+++
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunctionTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.hashicorp.vault;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultTemplate;
+import org.springframework.vault.support.VaultResponse;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Lookups of the properties function against a fake {@link VaultTemplate},
which answers the KV v2 data paths.
+ */
+class HashicorpVaultPropertiesFunctionTest {
+
+ @Test
+ void lookupWithoutEngineDoesNotUseTheEngineOfThePreviousLookup() {
+ FakeVaultTemplate vault = new FakeVaultTemplate(
+ Map.of(
+ "kv/data/api", Map.of("password", "from-kv"),
+ "secret/data/api", Map.of("password", "from-secret")));
+ HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction(vault);
+
+ assertEquals("from-kv", function.apply("kv:api#password"));
+ // no engine: the default 'secret' engine, not the 'kv' engine of the
lookup before
+ assertEquals("from-secret", function.apply("api#password"));
+ assertEquals("{password=from-secret}", function.apply("api"));
+ assertEquals("from-kv", function.apply("kv:api#password"));
+
+ assertEquals(List.of("kv/data/api", "secret/data/api",
"secret/data/api", "kv/data/api"), vault.reads);
+ }
+
+ @Test
+ void lookupWithoutEngineWithVersion() {
+ FakeVaultTemplate vault = new FakeVaultTemplate(
+ Map.of(
+ "secret/data/api?version=2", Map.of("password",
"v2")));
+ HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction(vault);
+
+ assertEquals("{password=v2}", function.apply("api@2"));
+ assertEquals("v2", function.apply("api#password@2"));
+ assertEquals("v2", function.apply("secret:api#password@2"));
+ }
+
+ @Test
+ void concurrentLookupsReadFromTheirOwnEngine() throws Exception {
+ FakeVaultTemplate vault = new FakeVaultTemplate(
+ Map.of(
+ "engine1/data/db", Map.of("password", "engine1"),
+ "engine2/data/db", Map.of("password", "engine2"),
+ "secret/data/db", Map.of("password", "secret")));
+ HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction(vault);
+
+ List<String> mismatches = new CopyOnWriteArrayList<>();
+ List<Callable<Void>> tasks = new ArrayList<>();
+ for (String engine : List.of("engine1", "engine2", "secret",
"engine1", "engine2", "secret")) {
+ String property = "secret".equals(engine) ? "db#password" : engine
+ ":db#password";
+ tasks.add(() -> {
+ for (int i = 0; i < 500; i++) {
+ String value = function.apply(property);
+ if (!engine.equals(value)) {
+ mismatches.add(property + " -> " + value);
+ }
+ }
+ return null;
+ });
+ }
+
+ ExecutorService executor = Executors.newFixedThreadPool(tasks.size());
+ try {
+ for (Future<Void> future : executor.invokeAll(tasks, 30,
TimeUnit.SECONDS)) {
+ future.get();
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ assertTrue(mismatches.isEmpty(), "lookups read from the wrong engine:
" + mismatches);
+ }
+
+ @Test
+ void missingSecretWithFieldReturnsNull() {
+ HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction(new FakeVaultTemplate(Map.of()));
+
+ // same as a missing secret without a field, so optional placeholders
work
+ assertNull(function.apply("secret:missing"));
+ assertNull(function.apply("secret:missing#password"));
+ assertNull(function.apply("secret:missing#password@2"));
+ }
+
+ @Test
+ void missingSecretWithFieldReturnsDefaultValue() {
+ HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction(new FakeVaultTemplate(Map.of()));
+
+ assertEquals("admin", function.apply("secret:missing#username:admin"));
+ assertEquals("admin",
function.apply("secret:missing#username:admin@2"));
+ }
+
+ @Test
+ void missingFieldReturnsNullOrDefaultValue() {
+ HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction(
+ new FakeVaultTemplate(Map.of("secret/data/database",
Map.of("password", "pw"))));
+
+ assertEquals("pw", function.apply("secret:database#password"));
+ assertNull(function.apply("secret:database#username"));
+ assertEquals("admin",
function.apply("secret:database#username:admin"));
+ }
+
+ @Test
+ void responseWithoutDataReturnsNullOrDefaultValue() {
+ FakeVaultTemplate vault = new FakeVaultTemplate(Map.of());
+ // a response without any data, and a response whose data has no
secret data
+ vault.responses.put("secret/data/nodata", new VaultResponse());
+ VaultResponse onlyMetadata = new VaultResponse();
+ onlyMetadata.setData(Map.of("metadata", Map.of("version", 1)));
+ vault.responses.put("secret/data/onlymetadata", onlyMetadata);
+ HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction(vault);
+
+ assertNull(function.apply("secret:nodata"));
+ assertNull(function.apply("secret:nodata#password"));
+ assertEquals("admin", function.apply("secret:nodata#password:admin"));
+ assertNull(function.apply("secret:onlymetadata"));
+ assertNull(function.apply("secret:onlymetadata#password"));
+ assertEquals("admin",
function.apply("secret:onlymetadata#password:admin"));
+ }
+
+ private static final class FakeVaultTemplate extends VaultTemplate {
+
+ private final Map<String, Map<String, Object>> secrets;
+ private final List<String> reads = new CopyOnWriteArrayList<>();
+ // raw responses by path, for responses that are not a regular secret
+ private final Map<String, VaultResponse> responses = new
ConcurrentHashMap<>();
+
+ FakeVaultTemplate(Map<String, Map<String, Object>> secrets) {
+ super(VaultEndpoint.create("localhost", 8200), new
TokenAuthentication("token"));
+ this.secrets = secrets;
+ }
+
+ @Override
+ public VaultResponse read(String path) {
+ reads.add(path);
+ if (responses.containsKey(path)) {
+ return responses.get(path);
+ }
+ Map<String, Object> data = secrets.get(path);
+ if (data == null) {
+ // as Vault answers 404 for a secret that does not exist
+ return null;
+ }
+ VaultResponse response = new VaultResponse();
+ response.setData(Map.of("data", data));
+ return response;
+ }
+ }
+}
diff --git
a/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
index d8385f86d624..3c362e8a08f1 100644
---
a/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
+++
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
@@ -201,7 +201,7 @@ class HashicorpVaultReloadTriggerTaskTest extends
CamelTestSupport {
@Test
void secretWithoutEngineIsTrackedAsDefaultEngine() {
- // a fresh function has no engine yet, so a plain hashicorp:api lookup
runs with a null engine
+ // a plain hashicorp:api lookup has no engine, so it reads from the
default secret engine
HashicorpVaultPropertiesFunction function = new
HashicorpVaultPropertiesFunction();
function.setCamelContext(context);
function.start();
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
new file mode 100644
index 000000000000..5a3c1009e3ab
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -0,0 +1,29 @@
+= Apache Camel 4.x Upgrade Guide
+
+This document is for helping you upgrade your Apache Camel application
+from Camel 4.x to 4.y. For example, if you are upgrading Camel 4.0 to 4.2,
then you should follow the guides
+from both 4.0 to 4.1 and 4.1 to 4.2.
+
+[NOTE]
+====
+https://github.com/apache/camel-upgrade-recipes/[The Camel Upgrade Recipes
project] provides automated assistance for some common migration tasks.
+Note that manual migration is still required.
+See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details.
+====
+
+== Upgrading Camel 4.23 to 4.24
+
+=== camel-hashicorp-vault - properties without engine use the `secret` engine
+
+A `hashicorp:` property without an engine, such as `{{hashicorp:mysecret}}` or
`{{hashicorp:mysecret#field}}`, now
+reads the secret from the `secret` engine. Before, it read from the engine of
the `hashicorp:` property resolved
+before it (or from a `null` engine), and `{{hashicorp:mysecret#field}}` always
resolved to no value. Properties with
+an engine, such as `{{hashicorp:secret:mysecret#field}}`, are not affected.
+
+A `hashicorp:` property without an engine and with a version, such as
`{{hashicorp:mysecret@2}}`, now reads version 2
+of the secret `mysecret` in the `secret` engine. Before, the `@2` was kept as
part of the secret name.
+
+A `hashicorp:` property with a field of a secret that does not exist, such as
`{{hashicorp:secret:missing#field}}`,
+now resolves to no value, as it already did without a field, so its default
value or an optional placeholder
+(`{{?hashicorp:secret:missing#field}}`) applies. Before, without a default
value, it failed with a
+`NullPointerException`.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
index 982e18bb2fba..91f8f833c7fe 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
@@ -32,6 +32,7 @@ You can find the upgrade guide for each release in the
following pages:
- xref:camel-4x-upgrade-guide-4_21.adoc[Upgrade guide 4.20 -> 4.21]
- xref:camel-4x-upgrade-guide-4_22.adoc[Upgrade guide 4.21 -> 4.22 LTS]
- xref:camel-4x-upgrade-guide-4_23.adoc[Upgrade guide 4.22 -> 4.23]
+- xref:camel-4x-upgrade-guide-4_24.adoc[Upgrade guide 4.23 -> 4.24]
[NOTE]
====