This is an automated email from the ASF dual-hosted git repository.
acosentino 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 ed0548278c45 CAMEL-22676 - Camel-Hashicorp-Vault: Support Secret
Refresh (#19868)
ed0548278c45 is described below
commit ed0548278c45f7c581a182c5bcfbd0acf4bee248
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 10 14:04:03 2025 +0100
CAMEL-22676 - Camel-Hashicorp-Vault: Support Secret Refresh (#19868)
* CAMEL-22676 - Camel-Hashicorp-Vault: Support Secret Refresh
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-22676 - Camel-Hashicorp-Vault: Support Secret Refresh
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-22676 - Camel-Hashicorp-Vault: Support Secret Refresh
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../main/camel-main-configuration-metadata.json | 3 +
.../camel/periodic-task/hashicorp-secret-refresh | 2 +
.../src/main/docs/hashicorp-vault-component.adoc | 59 ++++
.../vault/HashicorpVaultPropertiesFunction.java | 13 +
.../vault/HashicorpVaultReloadTriggerTask.java | 303 +++++++++++++++++++++
.../camel/vault/HashicorpVaultConfiguration.java | 39 +++
.../HashicorpVaultConfigurationConfigurer.java | 15 +
...corpVaultConfigurationPropertiesConfigurer.java | 18 ++
.../camel-main-configuration-metadata.json | 3 +
core/camel-main/src/main/docs/main.adoc | 5 +-
.../camel/main/DefaultConfigurationConfigurer.java | 19 ++
.../HashicorpVaultConfigurationProperties.java | 24 ++
docs/user-manual/modules/ROOT/pages/security.adoc | 70 +++++
13 files changed, 572 insertions(+), 1 deletion(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index 9da54a95bd7f..07521281b375 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -425,7 +425,10 @@
{ "name": "camel.vault.hashicorp.host", "required": false, "description":
"Host to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.hashicorp.namespace", "required": false,
"description": "If the Hashicorp Vault instance is deployed on Hashicorp Cloud,
this field will determine the namespace", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.hashicorp.port", "required": false, "description":
"Port to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
+ { "name": "camel.vault.hashicorp.refreshEnabled", "required": false,
"description": "Whether to automatically reload Camel upon secrets being
updated in Hashicorp Vault.", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
+ { "name": "camel.vault.hashicorp.refreshPeriod", "required": false,
"description": "The period (millis) between checking Hashicorp Vault for
updated secrets.", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "integer",
"javaType": "long", "defaultValue": 60000, "secret": false },
{ "name": "camel.vault.hashicorp.scheme", "required": false,
"description": "Scheme to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
+ { "name": "camel.vault.hashicorp.secrets", "required": false,
"description": "Specify the secret names (or pattern) to check for updates.
Multiple secrets can be separated by comma.", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.hashicorp.token", "required": false, "description":
"Token to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": true },
{ "name": "camel.vault.ibm.eventStreamBootstrapServers", "required":
false, "description": "Specify the Bootstrap servers for consuming notification
on IBM Event Stream. Multiple servers can be separated by comma.",
"sourceType": "org.apache.camel.vault.IBMSecretsManagerVaultConfiguration",
"type": "string", "javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.ibm.eventStreamConsumerPollTimeout", "required":
false, "description": "Specify the Consumer Poll Timeout while consuming from
IBM Event Stream Topic", "sourceType":
"org.apache.camel.vault.IBMSecretsManagerVaultConfiguration", "type":
"integer", "javaType": "long", "defaultValue": 3000, "secret": false },
diff --git
a/components/camel-hashicorp-vault/src/generated/resources/META-INF/services/org/apache/camel/periodic-task/hashicorp-secret-refresh
b/components/camel-hashicorp-vault/src/generated/resources/META-INF/services/org/apache/camel/periodic-task/hashicorp-secret-refresh
new file mode 100644
index 000000000000..74324fe0ada0
--- /dev/null
+++
b/components/camel-hashicorp-vault/src/generated/resources/META-INF/services/org/apache/camel/periodic-task/hashicorp-secret-refresh
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.hashicorp.vault.vault.HashicorpVaultReloadTriggerTask
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 2def23cd9bf0..15d0bdfc3c31 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
@@ -231,6 +231,65 @@ This approach will return the username field of the
database secret with version
The only requirement is adding the camel-hashicorp-vault jar to your Camel
application.
+=== Automatic Camel context reloading on Secret Refresh
+
+Being able to reload Camel context on a Secret Refresh could be done by
specifying the usual credentials (the same used for Hashicorp Vault Property
Function).
+
+With Environment variables:
+
+[source,bash]
+----
+export CAMEL_HASHICORP_VAULT_TOKEN=token
+export CAMEL_HASHICORP_VAULT_HOST=host
+export CAMEL_HASHICORP_VAULT_PORT=port
+export CAMEL_HASHICORP_VAULT_SCHEME=http/https
+----
+
+or as plain Camel main properties:
+
+[source,properties]
+----
+camel.vault.hashicorp.token = token
+camel.vault.hashicorp.host = host
+camel.vault.hashicorp.port = port
+camel.vault.hashicorp.scheme = scheme
+----
+
+To enable the automatic refresh, you'll need additional properties to set:
+
+[source,properties]
+----
+camel.vault.hashicorp.refreshEnabled=true
+camel.vault.hashicorp.refreshPeriod=60000
+camel.vault.hashicorp.secrets=database,api-keys
+camel.main.context-reload-enabled = true
+----
+
+where `camel.vault.hashicorp.refreshEnabled` will enable the automatic context
reload, `camel.vault.hashicorp.refreshPeriod` is the interval of time between
two different checks for update events (default 60000ms), and
`camel.vault.hashicorp.secrets` is a comma-separated list of secret names (or
patterns) to check for updates.
+
+The secret names should use the following format:
+- `mysecret` or `path/to/secret` - tracks a secret in the default `secret`
engine
+- `myengine:mysecret` or `myengine:path/to/secret` - tracks a secret in the
`myengine` engine (use `:` to separate engine from path)
+- You can use patterns like `database*` to match multiple secrets
+
+Note that `camel.vault.hashicorp.secrets` is not mandatory: if not specified
the task responsible for checking updates events will take into account all the
properties with a `hashicorp:` prefix.
+
+==== How the Refresh Mechanism Works
+
+Unlike cloud-based secret managers (AWS, GCP, Azure) that provide event-driven
notifications, Hashicorp Vault does not have a native event notification system
for secret changes. Therefore, this implementation uses a **polling-based
approach**:
+
+1. **Metadata Polling**: The refresh task periodically queries the Hashicorp
Vault metadata endpoint (`/v1/<engine>/metadata/<secret>`) for each tracked
secret.
+2. **Version Tracking**: It compares the `current_version` field to detect
changes.
+3. **Change Detection**: When a version change is detected, it triggers a
Camel context reload.
+4. **Efficiency**: Only metadata is queried (not the full secret content),
making this approach lightweight.
+
+For example, if a secret named `database` is updated in Vault:
+- The metadata endpoint returns `"current_version": 5`
+- On the next check, if `current_version` changes to `6`, a reload is triggered
+- The refresh period (default 60 seconds) determines how quickly changes are
detected
+
+This polling approach works with all Hashicorp Vault deployments (on-premise,
cloud, enterprise, and open-source) without requiring additional infrastructure.
+
include::spring-boot:partial$starter.adoc[]
=== Using Hashicorp Vault Property Function in Spring Boot for Early resolving
properties
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 5f4230f66ab5..60b40d0a1f3a 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
@@ -16,7 +16,9 @@
*/
package org.apache.camel.component.hashicorp.vault;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
@@ -82,6 +84,7 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
= "CAMEL_HASHICORP_VAULT_NAMESPACE";
private CamelContext camelContext;
private VaultTemplate client;
+ private final Set<String> secrets = new HashSet<>();
private String engine;
private String namespace;
@@ -207,6 +210,9 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
}
private String getSecretFromSource(String key, String subkey, String
defaultValue, String version) {
+ // capture name of secret
+ secrets.add(key);
+
String returnValue = null;
try {
String completePath = "";
@@ -254,4 +260,11 @@ public class HashicorpVaultPropertiesFunction extends
ServiceSupport implements
public CamelContext getCamelContext() {
return camelContext;
}
+
+ /**
+ * Ids of the secrets in use
+ */
+ public Set<String> getSecrets() {
+ return secrets;
+ }
}
diff --git
a/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/vault/HashicorpVaultReloadTriggerTask.java
b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/vault/HashicorpVaultReloadTriggerTask.java
new file mode 100644
index 000000000000..bdf634f61980
--- /dev/null
+++
b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/vault/HashicorpVaultReloadTriggerTask.java
@@ -0,0 +1,303 @@
+/*
+ * 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.vault;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.RuntimeCamelException;
+import
org.apache.camel.component.hashicorp.vault.HashicorpVaultPropertiesFunction;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spi.annotations.PeriodicTask;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.vault.HashicorpVaultConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultTemplate;
+import org.springframework.vault.support.VaultResponse;
+
+/**
+ * Period task which checks if Hashicorp Vault secrets has been updated and
can trigger Camel to be reloaded.
+ */
+@PeriodicTask("hashicorp-secret-refresh")
+public class HashicorpVaultReloadTriggerTask extends ServiceSupport implements
CamelContextAware, Runnable {
+
+ private static final String CAMEL_HASHICORP_VAULT_TOKEN_ENV =
"CAMEL_HASHICORP_VAULT_TOKEN";
+ private static final String CAMEL_HASHICORP_VAULT_HOST_ENV =
"CAMEL_HASHICORP_VAULT_HOST";
+ private static final String CAMEL_HASHICORP_VAULT_PORT_ENV =
"CAMEL_HASHICORP_VAULT_PORT";
+ private static final String CAMEL_HASHICORP_VAULT_SCHEME_ENV =
"CAMEL_HASHICORP_VAULT_SCHEME";
+ private static final String CAMEL_HASHICORP_VAULT_CLOUD_ENV =
"CAMEL_HASHICORP_VAULT_CLOUD";
+ private static final String CAMEL_HASHICORP_VAULT_NAMESPACE_ENV =
"CAMEL_HASHICORP_VAULT_NAMESPACE";
+
+ private static final Logger LOG =
LoggerFactory.getLogger(HashicorpVaultReloadTriggerTask.class);
+
+ private CamelContext camelContext;
+ private boolean reloadEnabled = true;
+ private String secrets;
+ private VaultTemplate client;
+ private HashicorpVaultPropertiesFunction propertiesFunction;
+ private volatile Instant lastCheckTime;
+ private volatile Instant lastReloadTime;
+ private final Map<String, Instant> updates = new HashMap<>();
+ private final Map<String, Integer> versionsMap = new HashMap<>();
+ private boolean cloud;
+ private String namespace;
+
+ public HashicorpVaultReloadTriggerTask() {
+ }
+
+ @Override
+ public CamelContext getCamelContext() {
+ return camelContext;
+ }
+
+ @Override
+ public void setCamelContext(CamelContext camelContext) {
+ this.camelContext = camelContext;
+ }
+
+ public boolean isReloadEnabled() {
+ return reloadEnabled;
+ }
+
+ /**
+ * Whether Camel should be reloaded on Hashicorp Vault secret updated
+ */
+ public void setReloadEnabled(boolean reloadEnabled) {
+ this.reloadEnabled = reloadEnabled;
+ }
+
+ /**
+ * A map of the updated secrets with the latest updated time.
+ */
+ public Map<String, Instant> getUpdates() {
+ return Collections.unmodifiableMap(updates);
+ }
+
+ /**
+ * Last time this task checked Hashicorp Vault for updated secrets.
+ */
+ public Instant getLastCheckTime() {
+ return lastCheckTime;
+ }
+
+ /**
+ * Last time Hashicorp Vault secrets update triggered reload.
+ */
+ public Instant getLastReloadTime() {
+ return lastReloadTime;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+
+ // auto-detect secrets in-use
+ PropertiesComponent pc = camelContext.getPropertiesComponent();
+ PropertiesFunction pf = pc.getPropertiesFunction("hashicorp");
+ if (pf instanceof HashicorpVaultPropertiesFunction) {
+ propertiesFunction = (HashicorpVaultPropertiesFunction) pf;
+ LOG.debug("Auto-detecting secrets from properties-function: {}",
pf.getName());
+ }
+ // specific secrets
+ HashicorpVaultConfiguration hashicorpVaultConfiguration =
getCamelContext().getVaultConfiguration().hashicorp();
+ secrets = hashicorpVaultConfiguration.getSecrets();
+ if (ObjectHelper.isEmpty(secrets) && propertiesFunction == null) {
+ throw new IllegalArgumentException("Secrets must be configured on
Hashicorp vault configuration");
+ }
+
+ String token = System.getenv(CAMEL_HASHICORP_VAULT_TOKEN_ENV);
+ String host = System.getenv(CAMEL_HASHICORP_VAULT_HOST_ENV);
+ String port = System.getenv(CAMEL_HASHICORP_VAULT_PORT_ENV);
+ String scheme = System.getenv(CAMEL_HASHICORP_VAULT_SCHEME_ENV);
+ if (System.getenv(CAMEL_HASHICORP_VAULT_CLOUD_ENV) != null) {
+ cloud =
Boolean.parseBoolean(System.getenv(CAMEL_HASHICORP_VAULT_CLOUD_ENV));
+ }
+ namespace = System.getenv(CAMEL_HASHICORP_VAULT_NAMESPACE_ENV);
+
+ if (ObjectHelper.isEmpty(token) && ObjectHelper.isEmpty(host)
+ && ObjectHelper.isEmpty(port) && ObjectHelper.isEmpty(scheme)
&& ObjectHelper.isEmpty(namespace)) {
+ if (ObjectHelper.isNotEmpty(hashicorpVaultConfiguration)) {
+ token = hashicorpVaultConfiguration.getToken();
+ host = hashicorpVaultConfiguration.getHost();
+ port = hashicorpVaultConfiguration.getPort();
+ scheme = hashicorpVaultConfiguration.getScheme();
+ cloud = hashicorpVaultConfiguration.isCloud();
+ if (hashicorpVaultConfiguration.isCloud()) {
+ namespace = hashicorpVaultConfiguration.getNamespace();
+ }
+ }
+ }
+
+ if (ObjectHelper.isNotEmpty(token) && ObjectHelper.isNotEmpty(host)
+ && ObjectHelper.isNotEmpty(port) &&
ObjectHelper.isNotEmpty(scheme)) {
+ VaultEndpoint vaultEndpoint = new VaultEndpoint();
+ vaultEndpoint.setHost(host);
+ vaultEndpoint.setPort(Integer.parseInt(port));
+ vaultEndpoint.setScheme(scheme);
+
+ client = new VaultTemplate(
+ vaultEndpoint,
+ new TokenAuthentication(token));
+ } else {
+ throw new RuntimeCamelException(
+ "Using the Hashicorp Vault Secrets Refresh Task requires
setting Token, Host, port and scheme properties");
+ }
+ }
+
+ @Override
+ protected void doShutdown() throws Exception {
+ super.doShutdown();
+
+ client = null;
+ updates.clear();
+ versionsMap.clear();
+ }
+
+ @Override
+ public void run() {
+ lastCheckTime = Instant.now();
+ boolean triggerReloading = false;
+
+ try {
+ // Get set of secrets to check
+ Set<String> secretsToCheck = new HashSet<>();
+ if (secrets != null) {
+ Collections.addAll(secretsToCheck, secrets.split(","));
+ }
+ if (propertiesFunction != null) {
+ secretsToCheck.addAll(propertiesFunction.getSecrets());
+ }
+
+ // Check each secret for updates
+ for (String secretName : secretsToCheck) {
+ if (matchSecret(secretName)) {
+ try {
+ // Query metadata endpoint to get current version
+ String metadataPath = buildMetadataPath(secretName);
+ VaultResponse response = client.read(metadataPath);
+
+ if (response != null && response.getData() != null) {
+ Object currentVersionObj =
response.getData().get("current_version");
+ if (currentVersionObj != null) {
+ Integer currentVersion =
Integer.valueOf(currentVersionObj.toString());
+ Integer lastKnownVersion =
versionsMap.get(secretName);
+
+ if (lastKnownVersion == null) {
+ // First time seeing this secret, just
record the version
+ versionsMap.put(secretName,
currentVersion);
+ LOG.debug("Tracking secret {} at version
{}", secretName, currentVersion);
+ } else if
(!currentVersion.equals(lastKnownVersion)) {
+ // Version changed, trigger reload
+ versionsMap.put(secretName,
currentVersion);
+ updates.put(secretName, Instant.now());
+ if (isReloadEnabled()) {
+ LOG.info(
+ "Update for Hashicorp Vault
secret: {} detected (version {} -> {}), triggering CamelContext reload",
+ secretName, lastKnownVersion,
currentVersion);
+ triggerReloading = true;
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOG.warn("Error checking secret {} for updates: {}.
Will retry on next run.",
+ secretName, e.getMessage());
+ LOG.debug("Exception details:", e);
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOG.warn(
+ "Error during Hashicorp Vault Secrets Refresh Task due to
{}. This exception is ignored. Will try again on next run.",
+ e.getMessage(), e);
+ }
+
+ if (triggerReloading) {
+ ContextReloadStrategy reload =
camelContext.hasService(ContextReloadStrategy.class);
+ if (reload != null) {
+ // trigger reload
+ lastReloadTime = Instant.now();
+ reload.onReload(this);
+ }
+ }
+ }
+
+ protected boolean matchSecret(String name) {
+ Set<String> set = new HashSet<>();
+ if (secrets != null) {
+ Collections.addAll(set, secrets.split(","));
+ }
+ if (propertiesFunction != null) {
+ set.addAll(propertiesFunction.getSecrets());
+ }
+
+ for (String part : set) {
+ boolean result = name.contains(part) ||
PatternHelper.matchPattern(name, part);
+ LOG.trace("Matching secret id: {}={} -> {}", name, part, result);
+ if (result) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private String buildMetadataPath(String secretName) {
+ // Extract engine and secret name from the secret identifier
+ // Format can be "engine:secretname" or just "secretname" (defaults to
"secret" engine)
+ // Use ':' as separator since secret names can contain '/'
+ String engine = "secret"; // default engine
+ String actualSecretName = secretName;
+
+ // Check if secretName contains ':' which separates engine from secret
path
+ if (secretName.contains(":")) {
+ int colonIndex = secretName.indexOf(':');
+ engine = secretName.substring(0, colonIndex);
+ actualSecretName = secretName.substring(colonIndex + 1);
+ }
+
+ String metadataPath;
+ if (!cloud) {
+ metadataPath = engine + "/" + "metadata" + "/" + actualSecretName;
+ } else {
+ if (ObjectHelper.isNotEmpty(namespace)) {
+ metadataPath = namespace + "/" + engine + "/" + "metadata" +
"/" + actualSecretName;
+ } else {
+ metadataPath = engine + "/" + "metadata" + "/" +
actualSecretName;
+ }
+ }
+ return metadataPath;
+ }
+
+ @Override
+ public String toString() {
+ return "Hashicorp Vault Secrets Refresh Task";
+ }
+}
diff --git
a/core/camel-api/src/main/java/org/apache/camel/vault/HashicorpVaultConfiguration.java
b/core/camel-api/src/main/java/org/apache/camel/vault/HashicorpVaultConfiguration.java
index 26fa28aad5cc..0d081e034a00 100644
---
a/core/camel-api/src/main/java/org/apache/camel/vault/HashicorpVaultConfiguration.java
+++
b/core/camel-api/src/main/java/org/apache/camel/vault/HashicorpVaultConfiguration.java
@@ -35,6 +35,12 @@ public class HashicorpVaultConfiguration extends
VaultConfiguration {
private boolean cloud;
@Metadata
private String namespace;
+ @Metadata
+ private boolean refreshEnabled;
+ @Metadata(defaultValue = "60000")
+ private long refreshPeriod = 60000;
+ @Metadata
+ private String secrets;
public String getToken() {
return token;
@@ -101,4 +107,37 @@ public class HashicorpVaultConfiguration extends
VaultConfiguration {
public void setNamespace(String namespace) {
this.namespace = namespace;
}
+
+ public boolean isRefreshEnabled() {
+ return refreshEnabled;
+ }
+
+ /**
+ * Whether to automatically reload Camel upon secrets being updated in
Hashicorp Vault.
+ */
+ public void setRefreshEnabled(boolean refreshEnabled) {
+ this.refreshEnabled = refreshEnabled;
+ }
+
+ public long getRefreshPeriod() {
+ return refreshPeriod;
+ }
+
+ /**
+ * The period (millis) between checking Hashicorp Vault for updated
secrets.
+ */
+ public void setRefreshPeriod(long refreshPeriod) {
+ this.refreshPeriod = refreshPeriod;
+ }
+
+ public String getSecrets() {
+ return secrets;
+ }
+
+ /**
+ * Specify the secret names (or pattern) to check for updates. Multiple
secrets can be separated by comma.
+ */
+ public void setSecrets(String secrets) {
+ this.secrets = secrets;
+ }
}
diff --git
a/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationConfigurer.java
b/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationConfigurer.java
index 1253d894fbd2..f411e923a635 100644
---
a/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationConfigurer.java
+++
b/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationConfigurer.java
@@ -43,7 +43,12 @@ public class HashicorpVaultConfigurationConfigurer extends
org.apache.camel.supp
case "kubernetesVaultConfiguration":
target.setKubernetesVaultConfiguration(property(camelContext,
org.apache.camel.vault.KubernetesVaultConfiguration.class, value)); return true;
case "namespace": target.setNamespace(property(camelContext,
java.lang.String.class, value)); return true;
case "port": target.setPort(property(camelContext,
java.lang.String.class, value)); return true;
+ case "refreshenabled":
+ case "refreshEnabled": target.setRefreshEnabled(property(camelContext,
boolean.class, value)); return true;
+ case "refreshperiod":
+ case "refreshPeriod": target.setRefreshPeriod(property(camelContext,
long.class, value)); return true;
case "scheme": target.setScheme(property(camelContext,
java.lang.String.class, value)); return true;
+ case "secrets": target.setSecrets(property(camelContext,
java.lang.String.class, value)); return true;
case "springcloudconfigconfiguration":
case "springCloudConfigConfiguration":
target.setSpringCloudConfigConfiguration(property(camelContext,
org.apache.camel.vault.SpringCloudConfigConfiguration.class, value)); return
true;
case "token": target.setToken(property(camelContext,
java.lang.String.class, value)); return true;
@@ -74,7 +79,12 @@ public class HashicorpVaultConfigurationConfigurer extends
org.apache.camel.supp
case "kubernetesVaultConfiguration": return
org.apache.camel.vault.KubernetesVaultConfiguration.class;
case "namespace": return java.lang.String.class;
case "port": return java.lang.String.class;
+ case "refreshenabled":
+ case "refreshEnabled": return boolean.class;
+ case "refreshperiod":
+ case "refreshPeriod": return long.class;
case "scheme": return java.lang.String.class;
+ case "secrets": return java.lang.String.class;
case "springcloudconfigconfiguration":
case "springCloudConfigConfiguration": return
org.apache.camel.vault.SpringCloudConfigConfiguration.class;
case "token": return java.lang.String.class;
@@ -106,7 +116,12 @@ public class HashicorpVaultConfigurationConfigurer extends
org.apache.camel.supp
case "kubernetesVaultConfiguration": return
target.getKubernetesVaultConfiguration();
case "namespace": return target.getNamespace();
case "port": return target.getPort();
+ case "refreshenabled":
+ case "refreshEnabled": return target.isRefreshEnabled();
+ case "refreshperiod":
+ case "refreshPeriod": return target.getRefreshPeriod();
case "scheme": return target.getScheme();
+ case "secrets": return target.getSecrets();
case "springcloudconfigconfiguration":
case "springCloudConfigConfiguration": return
target.getSpringCloudConfigConfiguration();
case "token": return target.getToken();
diff --git
a/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationPropertiesConfigurer.java
b/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationPropertiesConfigurer.java
index 300340992595..02d874cea74c 100644
---
a/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationPropertiesConfigurer.java
+++
b/core/camel-main/src/generated/java/org/apache/camel/main/HashicorpVaultConfigurationPropertiesConfigurer.java
@@ -34,7 +34,10 @@ public class HashicorpVaultConfigurationPropertiesConfigurer
extends org.apache.
map.put("KubernetesVaultConfiguration",
org.apache.camel.vault.KubernetesVaultConfiguration.class);
map.put("Namespace", java.lang.String.class);
map.put("Port", java.lang.String.class);
+ map.put("RefreshEnabled", boolean.class);
+ map.put("RefreshPeriod", long.class);
map.put("Scheme", java.lang.String.class);
+ map.put("Secrets", java.lang.String.class);
map.put("SpringCloudConfigConfiguration",
org.apache.camel.vault.SpringCloudConfigConfiguration.class);
map.put("Token", java.lang.String.class);
ALL_OPTIONS = map;
@@ -64,7 +67,12 @@ public class HashicorpVaultConfigurationPropertiesConfigurer
extends org.apache.
case "kubernetesVaultConfiguration":
target.setKubernetesVaultConfiguration(property(camelContext,
org.apache.camel.vault.KubernetesVaultConfiguration.class, value)); return true;
case "namespace": target.setNamespace(property(camelContext,
java.lang.String.class, value)); return true;
case "port": target.setPort(property(camelContext,
java.lang.String.class, value)); return true;
+ case "refreshenabled":
+ case "refreshEnabled": target.setRefreshEnabled(property(camelContext,
boolean.class, value)); return true;
+ case "refreshperiod":
+ case "refreshPeriod": target.setRefreshPeriod(property(camelContext,
long.class, value)); return true;
case "scheme": target.setScheme(property(camelContext,
java.lang.String.class, value)); return true;
+ case "secrets": target.setSecrets(property(camelContext,
java.lang.String.class, value)); return true;
case "springcloudconfigconfiguration":
case "springCloudConfigConfiguration":
target.setSpringCloudConfigConfiguration(property(camelContext,
org.apache.camel.vault.SpringCloudConfigConfiguration.class, value)); return
true;
case "token": target.setToken(property(camelContext,
java.lang.String.class, value)); return true;
@@ -100,7 +108,12 @@ public class
HashicorpVaultConfigurationPropertiesConfigurer extends org.apache.
case "kubernetesVaultConfiguration": return
org.apache.camel.vault.KubernetesVaultConfiguration.class;
case "namespace": return java.lang.String.class;
case "port": return java.lang.String.class;
+ case "refreshenabled":
+ case "refreshEnabled": return boolean.class;
+ case "refreshperiod":
+ case "refreshPeriod": return long.class;
case "scheme": return java.lang.String.class;
+ case "secrets": return java.lang.String.class;
case "springcloudconfigconfiguration":
case "springCloudConfigConfiguration": return
org.apache.camel.vault.SpringCloudConfigConfiguration.class;
case "token": return java.lang.String.class;
@@ -132,7 +145,12 @@ public class
HashicorpVaultConfigurationPropertiesConfigurer extends org.apache.
case "kubernetesVaultConfiguration": return
target.getKubernetesVaultConfiguration();
case "namespace": return target.getNamespace();
case "port": return target.getPort();
+ case "refreshenabled":
+ case "refreshEnabled": return target.isRefreshEnabled();
+ case "refreshperiod":
+ case "refreshPeriod": return target.getRefreshPeriod();
case "scheme": return target.getScheme();
+ case "secrets": return target.getSecrets();
case "springcloudconfigconfiguration":
case "springCloudConfigConfiguration": return
target.getSpringCloudConfigConfiguration();
case "token": return target.getToken();
diff --git
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index 9da54a95bd7f..07521281b375 100644
---
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -425,7 +425,10 @@
{ "name": "camel.vault.hashicorp.host", "required": false, "description":
"Host to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.hashicorp.namespace", "required": false,
"description": "If the Hashicorp Vault instance is deployed on Hashicorp Cloud,
this field will determine the namespace", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.hashicorp.port", "required": false, "description":
"Port to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
+ { "name": "camel.vault.hashicorp.refreshEnabled", "required": false,
"description": "Whether to automatically reload Camel upon secrets being
updated in Hashicorp Vault.", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
+ { "name": "camel.vault.hashicorp.refreshPeriod", "required": false,
"description": "The period (millis) between checking Hashicorp Vault for
updated secrets.", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "integer",
"javaType": "long", "defaultValue": 60000, "secret": false },
{ "name": "camel.vault.hashicorp.scheme", "required": false,
"description": "Scheme to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
+ { "name": "camel.vault.hashicorp.secrets", "required": false,
"description": "Specify the secret names (or pattern) to check for updates.
Multiple secrets can be separated by comma.", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.hashicorp.token", "required": false, "description":
"Token to access hashicorp vault", "sourceType":
"org.apache.camel.vault.HashicorpVaultConfiguration", "type": "string",
"javaType": "java.lang.String", "secret": true },
{ "name": "camel.vault.ibm.eventStreamBootstrapServers", "required":
false, "description": "Specify the Bootstrap servers for consuming notification
on IBM Event Stream. Multiple servers can be separated by comma.",
"sourceType": "org.apache.camel.vault.IBMSecretsManagerVaultConfiguration",
"type": "string", "javaType": "java.lang.String", "secret": false },
{ "name": "camel.vault.ibm.eventStreamConsumerPollTimeout", "required":
false, "description": "Specify the Consumer Poll Timeout while consuming from
IBM Event Stream Topic", "sourceType":
"org.apache.camel.vault.IBMSecretsManagerVaultConfiguration", "type":
"integer", "javaType": "long", "defaultValue": 3000, "secret": false },
diff --git a/core/camel-main/src/main/docs/main.adoc
b/core/camel-main/src/main/docs/main.adoc
index 3baca5cebc5e..cceb9356acb8 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -483,7 +483,7 @@ The camel.vault.kubernetescm supports 2 options, which are
listed below.
=== Camel Hashicorp Vault configurations
-The camel.vault.hashicorp supports 6 options, which are listed below.
+The camel.vault.hashicorp supports 9 options, which are listed below.
[width="100%",cols="2,5,^1,2",options="header"]
|===
@@ -492,7 +492,10 @@ The camel.vault.hashicorp supports 6 options, which are
listed below.
| *camel.vault.hashicorp.host* | Host to access hashicorp vault | | String
| *camel.vault.hashicorp.namespace* | If the Hashicorp Vault instance is
deployed on Hashicorp Cloud, this field will determine the namespace | | String
| *camel.vault.hashicorp.port* | Port to access hashicorp vault | | String
+| *camel.vault.hashicorp.refresh{zwsp}Enabled* | Whether to automatically
reload Camel upon secrets being updated in Hashicorp Vault. | false | boolean
+| *camel.vault.hashicorp.refresh{zwsp}Period* | The period (millis) between
checking Hashicorp Vault for updated secrets. | 60000 | long
| *camel.vault.hashicorp.scheme* | Scheme to access hashicorp vault | | String
+| *camel.vault.hashicorp.secrets* | Specify the secret names (or pattern) to
check for updates. Multiple secrets can be separated by comma. | | String
| *camel.vault.hashicorp.token* | Token to access hashicorp vault | | String
|===
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
index 63bb32b3f063..311b7dfedbff 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
@@ -860,6 +860,25 @@ public final class DefaultConfigurationConfigurer {
}
}
+ if (vc.hashicorp().isRefreshEnabled()) {
+ Optional<Runnable> task =
PluginHelper.getPeriodTaskResolver(camelContext)
+ .newInstance("hashicorp-secret-refresh", Runnable.class);
+ if (task.isPresent()) {
+ long period = vc.hashicorp().getRefreshPeriod();
+ Runnable r = task.get();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Scheduling: {} (period: {})", r,
TimeUtils.printDuration(period, false));
+ }
+ if (camelContext.hasService(ContextReloadStrategy.class) ==
null) {
+ // refresh is enabled then we need to automatically enable
context-reload as well
+ ContextReloadStrategy reloader = new
DefaultContextReloadStrategy();
+ camelContext.addService(reloader);
+ }
+ PeriodTaskScheduler scheduler =
PluginHelper.getPeriodTaskScheduler(camelContext);
+ scheduler.schedulePeriodTask(r, period);
+ }
+ }
+
if (vc.springConfig().isRefreshEnabled()) {
Optional<Runnable> task =
PluginHelper.getPeriodTaskResolver(camelContext)
.newInstance("spring-config-refresh", Runnable.class);
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/HashicorpVaultConfigurationProperties.java
b/core/camel-main/src/main/java/org/apache/camel/main/HashicorpVaultConfigurationProperties.java
index d11827dc915b..4f5a4cc15ece 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/HashicorpVaultConfigurationProperties.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/HashicorpVaultConfigurationProperties.java
@@ -97,4 +97,28 @@ public class HashicorpVaultConfigurationProperties extends
HashicorpVaultConfigu
return this;
}
+ /**
+ * Whether to automatically reload Camel upon secrets being updated in
Hashicorp Vault
+ */
+ public HashicorpVaultConfigurationProperties withRefreshEnabled(boolean
refreshEnabled) {
+ setRefreshEnabled(refreshEnabled);
+ return this;
+ }
+
+ /**
+ * The period (millis) between checking Hashicorp Vault for updated secrets
+ */
+ public HashicorpVaultConfigurationProperties withRefreshPeriod(long
refreshPeriod) {
+ setRefreshPeriod(refreshPeriod);
+ return this;
+ }
+
+ /**
+ * Specify the secret names (or pattern) to check for updates. Multiple
secrets can be separated by comma
+ */
+ public HashicorpVaultConfigurationProperties withSecrets(String secrets) {
+ setSecrets(secrets);
+ return this;
+ }
+
}
diff --git a/docs/user-manual/modules/ROOT/pages/security.adoc
b/docs/user-manual/modules/ROOT/pages/security.adoc
index 1fe9ba02cd65..e0444ec69420 100644
--- a/docs/user-manual/modules/ROOT/pages/security.adoc
+++ b/docs/user-manual/modules/ROOT/pages/security.adoc
@@ -1125,3 +1125,73 @@ where `camel.vault.ibm.eventStreamBootstrapServers` is
the comma-separated list
Note that `camel.vault.ibm.secrets` is not mandatory: if not specified the
task responsible for checking updates events will take into accounts or the
properties with an `ibm:` prefix.
The only requirement is adding the camel-ibm-secrets-manager jar to your Camel
application.
+
+==== Automatic Camel context reloading on Secret Refresh while using Hashicorp
Vault
+
+Being able to reload Camel context on a Secret Refresh could be done by
specifying the usual credentials (the same used for Hashicorp Vault Property
Function).
+
+With Environment variables:
+
+[source,bash]
+----
+export CAMEL_VAULT_HASHICORP_TOKEN=token
+export CAMEL_VAULT_HASHICORP_HOST=host
+export CAMEL_VAULT_HASHICORP_PORT=port
+export CAMEL_VAULT_HASHICORP_SCHEME=http/https
+----
+
+or as plain Camel main properties:
+
+[source,properties]
+----
+camel.vault.hashicorp.token = token
+camel.vault.hashicorp.host = host
+camel.vault.hashicorp.port = port
+camel.vault.hashicorp.scheme = scheme
+----
+
+To enable the automatic refresh, you'll need additional properties to set:
+
+[source,properties]
+----
+camel.vault.hashicorp.refreshEnabled=true
+camel.vault.hashicorp.refreshPeriod=60000
+camel.vault.hashicorp.secrets=database,api-keys
+camel.main.context-reload-enabled = true
+----
+
+where `camel.vault.hashicorp.refreshEnabled` will enable the automatic context
reload, `camel.vault.hashicorp.refreshPeriod` is the interval of time between
two different checks for update events (default 60000ms), and
`camel.vault.hashicorp.secrets` is a comma-separated list of secret names (or
patterns) to check for updates.
+
+The secret names should use the following format:
+
+- `mysecret` or `path/to/secret` - tracks a secret in the default `secret`
engine
+- `myengine:mysecret` or `myengine:path/to/secret` - tracks a secret in the
`myengine` engine (use `:` to separate engine from path)
+- You can use patterns like `database*` to match multiple secrets
+
+For example, to track secrets `omsecret/test` in the default `secret` engine
and `myapp/credentials` in a custom `kv` engine:
+
+[source,properties]
+----
+camel.vault.hashicorp.secrets=omsecret/test,kv:myapp/credentials
+----
+
+Note that `camel.vault.hashicorp.secrets` is not mandatory: if not specified
the task responsible for checking updates events will take into account all the
properties with a `hashicorp:` prefix.
+
+===== How the Refresh Mechanism Works
+
+Unlike cloud-based secret managers (AWS, GCP, Azure) that provide event-driven
notifications, Hashicorp Vault does not have a native event notification system
for secret changes. Therefore, this implementation uses a *polling-based
approach*:
+
+1. *Metadata Polling*: The refresh task periodically queries the Hashicorp
Vault metadata endpoint (`/v1/<engine>/metadata/<secret>`) for each tracked
secret.
+2. *Version Tracking*: It compares the `current_version` field to detect
changes.
+3. *Change Detection*: When a version change is detected, it triggers a Camel
context reload.
+4. *Efficiency*: Only metadata is queried (not the full secret content),
making this approach lightweight.
+
+For example, if a secret named `database` is updated in Vault:
+
+- The metadata endpoint returns `"current_version": 5`
+- On the next check, if `current_version` changes to `6`, a reload is triggered
+- The refresh period (default 60 seconds) determines how quickly changes are
detected
+
+This polling approach works with all Hashicorp Vault deployments (on-premise,
cloud, enterprise, and open-source) without requiring additional infrastructure.
+
+The only requirement is adding the camel-hashicorp-vault jar to your Camel
application.