Skip to content

CAMEL-26919: camel-hashicorp-vault-starter - the early resolution logs in with the camel.vault.hashicorp options - #2091

Open
oscerd wants to merge 1 commit into
apache:mainfrom
oscerd:feature/CAMEL-26919-hashicorp-vault-early-resolution
Open

oscerd wants to merge 1 commit into
apache:mainfrom
oscerd:feature/CAMEL-26919-hashicorp-vault-early-resolution

Conversation

@oscerd

@oscerd oscerd commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Summary

CAMEL-26919: the early resolution of hashicorp: properties (camel.component.hashicorp-vault.early-resolve-properties=true) built its own VaultTemplate with camel.vault.hashicorp.token. So the logins of CAMEL-25675 (AppRole, Kubernetes, JWT, cert, aws-iam, azure-msi, gcp-gce) and the trust store and key store options did not apply to it. With this change it logs in with the camel.vault.hashicorp options, as the properties function of the CamelContext does.

  • SpringBootHashicorpVaultPropertiesParser binds camel.vault.hashicorp to the camel-api HashicorpVaultConfiguration with the Spring Boot Binder. The relaxed names work as for the CamelContext (camel.vault.hashicorp.role-id), and every option of the Camel version is bound, including those not yet in HashicorpVaultConfigurationProperties (the cloud logins of CAMEL-25675: camel-hashicorp-vault - AWS IAM, Azure managed identity and GCP Compute Engine logins camel#29708). The parser creates the client with HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(...)) from CAMEL-26919: camel-hashicorp-vault - a Vault client without a CamelContext, for the Spring Boot early resolution camel#29819, which needs no CamelContext and renews its token on a daemon thread while the properties are resolved.
  • AbstractEarlyResolutionPropertiesParser calls a new release(PropertiesFunction) hook once the properties are resolved, also when they could not be. It does nothing by default, so the other vault parsers are unchanged. The hashicorp parser closes its client there, which stops the renewal and revokes the token of the login (a token given for the token login is not revoked).
  • The messages for a missing host, port, scheme or token (token login) are unchanged. An incomplete login, such as AppRole without a secret ID, fails with a RuntimeCamelException that names the camel.vault.hashicorp options.

Draft until apache/camel#29819 is merged and published in the 4.24.0-SNAPSHOT: this change uses its new methods, so the CI fails until then.

cc @Croway, since you mirrored the camel.vault.hashicorp options of CAMEL-25675 in HashicorpVaultConfigurationProperties. This PR changes only the early resolution. The cloud login options of apache/camel#29708 (awsIamServerId, awsRegion, awsStsEndpoint, azureResource, azureClientId) are not mirrored there yet: the early resolution binds them already, but the CamelContext gets them only from that class.

Tests

  • AbstractEarlyResolutionPropertiesParserTest: the properties function is released once the properties are resolved, and also when the resolution fails.
  • SpringBootHashicorpVaultPropertiesParserTest: an AppRole login with the kebab-case names needs no token, and an incomplete login fails as a RuntimeCamelException naming the options. The existing tests are unchanged.
  • EarlyResolvedPropertiesAppRoleTest: the early resolution logs in with AppRole against a Vault container, resolves the properties, and revokes the token of its login: no token of the AppRole login is left in auth/token/accessors. With the release of the parser disabled, the test fails on that check.
  • Built against the Camel artifacts of CAMEL-26919: camel-hashicorp-vault - a Vault client without a CamelContext, for the Spring Boot early resolution camel#29819: all 15 tests of AbstractEarlyResolutionPropertiesParserTest and all 12 tests of camel-hashicorp-vault-starter pass.

Claude Code on behalf of oscerd

🤖 Generated with Claude Code

…s in with the camel.vault.hashicorp options

The early resolution of hashicorp: properties built its own VaultTemplate with camel.vault.hashicorp.token,
so the logins of CAMEL-25675 (AppRole, Kubernetes, JWT, cert, cloud) and the TLS options did not apply to it.

- SpringBootHashicorpVaultPropertiesParser binds camel.vault.hashicorp to HashicorpVaultConfiguration with
  the Spring Boot Binder (relaxed names, such as camel.vault.hashicorp.role-id), and creates the client with
  HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(...)), which needs no
  CamelContext. The messages for a missing host, port, scheme or token are unchanged; an incomplete login
  fails with a RuntimeCamelException naming the camel.vault.hashicorp options.
- AbstractEarlyResolutionPropertiesParser calls a new release(PropertiesFunction) hook once the properties
  are resolved, also when they could not be. It does nothing by default; the hashicorp parser closes its
  client there, which stops the token renewal and revokes the token of the login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>

@Croway Croway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the early resolution change against apache/camel#29819 and Spring Vault 4.1.1. I haven't built or run it, since it needs camel#29819 published.

The main points:

  • On HCP Vault (cloud=true and a namespace) the secrets are read outside the namespace (line 105).
  • The early resolution and the CamelContext can use different settings: the CAMEL_HASHICORP_VAULT_* variables (line 70), and unresolved ${...} values that no longer fail fast (line 52).
  • A failed login is retried once per placeholder, which can trigger Vault's AppRole lockout (line 75), and AppRole/JWT logins now happen twice per startup (line 70).

Not repeated inline: the AWS/Azure login options missing from HashicorpVaultConfigurationProperties, which the description already mentions. Also, the description says draft until camel#29819 is published, but the PR isn't marked as a draft.

Claude Code (Opus 5.5) on behalf of Federico Mariani

private final HashicorpVaultClient client;

private EarlyResolutionFunction(HashicorpVaultClient client) {
super(client.getVaultTemplate());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HCP Vault: the secrets are read outside the namespace. super(client.getVaultTemplate()) uses the public HashicorpVaultPropertiesFunction(VaultTemplate) constructor, which leaves cloud=false and namespace=null. HashicorpVaultClient sends the login to the namespace (its loginClient), but apply() adds the namespace/ prefix to the secret path only when cloud and namespace are set.

So with camel.vault.hashicorp.cloud=true, namespace=admin and authentication=approle, the early resolution reads secret/data/key instead of admin/secret/data/key, gets a 403 and aborts startup, while the CamelContext reads the right path. The constructor that takes cloud and namespace is package-private in apache/camel#29819, so the starter can't pass them (see the comment on line 100).

Claude Code (Opus 5.5) on behalf of Federico Mariani

vaultEndpoint.setScheme(scheme);
HashicorpVaultClient client;
try {
client = HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early resolution and the CamelContext can use different settings. This uses HashicorpVaultClientSettings.fromConfiguration, while HashicorpVaultPropertiesFunction on the CamelContext uses fromEnvironmentOrConfiguration, where the CAMEL_HASHICORP_VAULT_* variables win as soon as one of them is set. The fromConfiguration Javadoc says it is meant for Spring Boot, but on Spring Boot the CamelContext function still reads those variables, so "as the properties function of the CamelContext does" in the class Javadoc doesn't hold there:

  • Only CAMEL_HASHICORP_VAULT_HOST / CAMEL_HASHICORP_VAULT_TOKEN set (as the Camel docs describe): the CamelContext resolves {{hashicorp:...}}, the early resolution fails with "Hashicorp Vault token is required".
  • Both set: the early resolution logs in with camel.vault.hashicorp.* (say AppRole), the CamelContext with the environment token.

Either use fromEnvironmentOrConfiguration here too, or document that the early resolution reads only camel.vault.hashicorp.*.

Claude Code (Opus 5.5) on behalf of Federico Mariani

throw new RuntimeCamelException(
"Cannot create the Hashicorp Vault client of the " + PREFIX + " options: " + e.getMessage(), e);
}
return new EarlyResolutionFunction(client);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failed login is retried once per placeholder. create doesn't log in; the LifecycleAwareSessionManager logs in on the first read and keeps no token when the login fails, so every apply() logs in again. The base class then goes through all the placeholders to report them together.

With a stale AppRole secret-id and 6 hashicorp: placeholders, one startup sends 6 failed auth/approle/login calls. Vault's user lockout covers AppRole (default threshold 5, 15 minutes), so the role_id is locked for every pod that shares it, the CamelContext login included, even after the secret-id is fixed. The old token-only path never logged in. Stopping after the first login failure, or logging in once in createPropertiesFunction, would avoid this.

Claude Code (Opus 5.5) on behalf of Federico Mariani

vaultEndpoint.setScheme(scheme);
HashicorpVaultClient client;
try {
client = HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppRole/JWT logins now happen twice per startup. The early resolution logs in with the same credentials that the CamelContext function and the secret refresh task use later. With secret_id_num_uses=1 (common with response-wrapped secret IDs), the early resolution uses up the secret ID, and the later CamelContext login fails with "invalid secret id": startup looks fine, then the hashicorp: lookups in routes fail. Worth at least a note in the docs.

Claude Code (Opus 5.5) on behalf of Federico Mariani

vaultEndpoint.setScheme(scheme);
HashicorpVaultClient client;
try {
client = HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the token login now sends an extra auth/token/lookup-self. Before, it used new VaultTemplate(endpoint, new TokenAuthentication(token)), i.e. a SimpleSessionManager. The client now goes through LifecycleAwareSessionManager, which does a token self-lookup by default. A token created with num_uses=N for N early-resolved secrets now runs out one read early (403 on the last), and a token whose policy lacks lookup-self logs "Cannot enhance VaultToken to a LoginToken" at WARN on every startup. It's a behaviour change for the token login, which the PR otherwise keeps as it was.

Claude Code (Opus 5.5) on behalf of Federico Mariani

HashicorpVaultClient client;
try {
client = HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
} catch (IllegalArgumentException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: only IllegalArgumentException gets the message that names the camel.vault.hashicorp options. Other failures from create, such as an IllegalStateException (or NoClassDefFoundError) when authentication=aws-iam and a needed AWS SDK module is missing, abort startup without that hint. Catching RuntimeException here would cover more cases.

Claude Code (Opus 5.5) on behalf of Federico Mariani

VaultTemplate root = root();
return root.list("auth/token/accessors").stream()
.filter(accessor -> "auth/approle/login".equals(
root.write("auth/token/lookup-accessor", Map.of("accessor", accessor))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible flake: if a token is revoked between list("auth/token/accessors") and lookup-accessor, Vault answers 400 "invalid accessor" and write throws a VaultException. untilAsserted retries only on AssertionError, so the await fails at once instead of polling again, and that window is exactly when the release revokes the token. .ignoreExceptions() on the await, or skipping accessors whose lookup fails, would make it stable.

Claude Code (Opus 5.5) on behalf of Federico Mariani

/**
* The properties function of the early resolution, with the client it reads the secrets with.
*/
private static final class EarlyResolutionFunction extends HashicorpVaultPropertiesFunction {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion for apache/camel#29819 rather than this PR. The private subclass and the instanceof check in release() work around HashicorpVaultPropertiesFunction not being able to take and own a HashicorpVaultClient. Because the subclass can pass only a VaultTemplate, it also loses cloud and namespace (comment on line 105). A public constructor or factory in camel#29819 that takes the client and its settings, and closes the client when the function stops, would fix both, and other runtimes wouldn't need to re-implement it.

Claude Code (Opus 5.5) on behalf of Federico Mariani

* Releases what the properties function holds, such as the login of its client, once the properties are resolved,
* also when they could not be. Does nothing by default.
*/
protected void release(PropertiesFunction propertiesFunction) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional, fine as a follow-up JIRA: only the hashicorp parser overrides release. The AWS parser (SecretsManagerClient, with its HTTP connection pool) and the GCP parser (SecretManagerServiceClient, with its gRPC channel and threads) still never close the clients of the early resolution. With this hook in place they could close them too, or the base class could close a function that is AutoCloseable.

Claude Code (Opus 5.5) on behalf of Federico Mariani

HashicorpVaultConfiguration configuration = Binder.get(environment)
.bind(PREFIX, HashicorpVaultConfiguration.class)
.orElseGet(HashicorpVaultConfiguration::new);
if (isTokenLogin(configuration)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: isTokenLogin, the host/port/scheme/token checks and the Integer.parseInt repeat what HashicorpVaultClient.create already does through HashicorpVaultClient.validate(settings) and HashicorpVaultAuthentication.of. If Camel changes the default login or how authentication is normalised, isTokenLogin drifts silently. Calling validate and mapping its IllegalArgumentException to the current messages would keep one copy of the rules.

Claude Code (Opus 5.5) on behalf of Federico Mariani

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants