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 39f53df14149 CAMEL-26256: camel-cyberark-vault - retry a rejected
token once and only when a new one can be obtained, and check the credentials
at endpoint start (#29209)
39f53df14149 is described below
commit 39f53df141491ccd4c491d0086f7e41185ee8820
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 5 13:01:24 2026 +0100
CAMEL-26256: camel-cyberark-vault - retry a rejected token once and only
when a new one can be obtained, and check the credentials at endpoint start
(#29209)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../catalog/docs/cyberark-vault-component.adoc | 8 +
.../src/main/docs/cyberark-vault-component.adoc | 8 +
.../cyberark/vault/CyberArkVaultEndpoint.java | 15 ++
.../vault/client/impl/ConjurClientImpl.java | 30 +++-
.../CyberArkVaultEndpointCredentialsTest.java | 122 ++++++++++++++
.../impl/ConjurClientReauthenticationTest.java | 177 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 14 ++
7 files changed, 367 insertions(+), 7 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cyberark-vault-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cyberark-vault-component.adoc
index 7f806962a634..7c8735d9c962 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cyberark-vault-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cyberark-vault-component.adoc
@@ -48,6 +48,14 @@ The component supports multiple authentication methods with
CyberArk Conjur:
2. **API Key Authentication**: Use username and API key (recommended for
programmatic access)
3. **Pre-authenticated Token**: Use a pre-obtained authentication token
+The endpoint fails to start when it has no usable credentials (an `authToken`,
or a `username` with an `apiKey` or a
+`password`) and no `conjurClient` is set.
+
+When Conjur rejects the access token with HTTP 401, for example because it has
expired, the component authenticates
+again with the API key or the password, and retries the call once. A
pre-authenticated token cannot be renewed: the
+call fails with the 401 response, and a new token has to be configured. The
`authToken` takes precedence: when it is
+set, the component uses only that token, also when a `username` with an
`apiKey` or a `password` is set as well.
+
=== Using CyberArk Conjur Vault Property Function
To use this function, you'll need to provide credentials to CyberArk Conjur
Service as environment variables:
diff --git
a/components/camel-cyberark-vault/src/main/docs/cyberark-vault-component.adoc
b/components/camel-cyberark-vault/src/main/docs/cyberark-vault-component.adoc
index 7f806962a634..7c8735d9c962 100644
---
a/components/camel-cyberark-vault/src/main/docs/cyberark-vault-component.adoc
+++
b/components/camel-cyberark-vault/src/main/docs/cyberark-vault-component.adoc
@@ -48,6 +48,14 @@ The component supports multiple authentication methods with
CyberArk Conjur:
2. **API Key Authentication**: Use username and API key (recommended for
programmatic access)
3. **Pre-authenticated Token**: Use a pre-obtained authentication token
+The endpoint fails to start when it has no usable credentials (an `authToken`,
or a `username` with an `apiKey` or a
+`password`) and no `conjurClient` is set.
+
+When Conjur rejects the access token with HTTP 401, for example because it has
expired, the component authenticates
+again with the API key or the password, and retries the call once. A
pre-authenticated token cannot be renewed: the
+call fails with the 401 response, and a new token has to be configured. The
`authToken` takes precedence: when it is
+set, the component uses only that token, also when a `username` with an
`apiKey` or a `password` is set as well.
+
=== Using CyberArk Conjur Vault Property Function
To use this function, you'll need to provide credentials to CyberArk Conjur
Service as environment variables:
diff --git
a/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/CyberArkVaultEndpoint.java
b/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/CyberArkVaultEndpoint.java
index 7a522ae93f23..2a2b05e5b482 100644
---
a/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/CyberArkVaultEndpoint.java
+++
b/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/CyberArkVaultEndpoint.java
@@ -84,6 +84,10 @@ public class CyberArkVaultEndpoint extends DefaultEndpoint
implements EndpointSe
protected void doStart() throws Exception {
super.doStart();
+ if (configuration.getConjurClient() == null) {
+ checkCredentials();
+ }
+
conjurClient = configuration.getConjurClient();
if (conjurClient == null) {
String url = configuration.getUrl();
@@ -112,6 +116,17 @@ public class CyberArkVaultEndpoint extends DefaultEndpoint
implements EndpointSe
}
}
+ private void checkCredentials() {
+ boolean hasLogin = ObjectHelper.isNotEmpty(configuration.getUsername())
+ && (ObjectHelper.isNotEmpty(configuration.getApiKey()) ||
ObjectHelper.isNotEmpty(configuration.getPassword()));
+ if (ObjectHelper.isEmpty(configuration.getAuthToken()) && !hasLogin) {
+ throw new IllegalArgumentException(
+ "No credentials to authenticate with CyberArk Conjur: set
the authToken option, the username and apiKey"
+ + " options, or the username
and password options, or set the conjurClient"
+ + " option");
+ }
+ }
+
public CyberArkVaultConfiguration getConfiguration() {
return configuration;
}
diff --git
a/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
b/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
index 339f3154e367..d0d4af414f43 100644
---
a/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
+++
b/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
@@ -42,7 +42,7 @@ public class ConjurClientImpl implements ConjurClient {
private final String username;
private final String password;
private final String apiKey;
- private String authToken;
+ private volatile String authToken;
private final HttpClient httpClient;
public ConjurClientImpl(
@@ -66,6 +66,10 @@ public class ConjurClientImpl implements ConjurClient {
@Override
public String retrieveSecret(String secretId, String version) {
+ return retrieveSecret(secretId, version, true);
+ }
+
+ private String retrieveSecret(String secretId, String version, boolean
retryOnUnauthorized) {
try {
// Ensure we have a valid token
if (authToken == null) {
@@ -94,11 +98,11 @@ public class ConjurClientImpl implements ConjurClient {
if (response.statusCode() == 200) {
return response.body();
- } else if (response.statusCode() == 401) {
- // Token expired, re-authenticate and retry
+ } else if (response.statusCode() == 401 && retryOnUnauthorized &&
canReauthenticate()) {
+ // Token expired, re-authenticate and retry once
LOG.debug("Token expired, re-authenticating");
authToken = authenticate();
- return retrieveSecret(secretId, version);
+ return retrieveSecret(secretId, version, false);
} else {
throw new IOException(
"Failed to retrieve secret: HTTP " +
response.statusCode() + " - " + response.body());
@@ -157,6 +161,10 @@ public class ConjurClientImpl implements ConjurClient {
@Override
public void createSecret(String secretId, String secretValue) {
+ createSecret(secretId, secretValue, true);
+ }
+
+ private void createSecret(String secretId, String secretValue, boolean
retryOnUnauthorized) {
try {
// Ensure we have a valid token
if (authToken == null) {
@@ -182,11 +190,11 @@ public class ConjurClientImpl implements ConjurClient {
if (response.statusCode() == 201 || response.statusCode() == 200) {
LOG.debug("Successfully created/updated secret: {}", secretId);
- } else if (response.statusCode() == 401) {
- // Token expired, re-authenticate and retry
+ } else if (response.statusCode() == 401 && retryOnUnauthorized &&
canReauthenticate()) {
+ // Token expired, re-authenticate and retry once
LOG.debug("Token expired, re-authenticating");
authToken = authenticate();
- createSecret(secretId, secretValue);
+ createSecret(secretId, secretValue, false);
} else {
throw new IOException(
"Failed to create/update secret: HTTP " +
response.statusCode() + " - " + response.body());
@@ -196,6 +204,14 @@ public class ConjurClientImpl implements ConjurClient {
}
}
+ /**
+ * Whether a new access token can be obtained after Conjur rejects the
current one. That needs an API key or a
+ * password: a client that only has a pre-authenticated token cannot get a
new token.
+ */
+ private boolean canReauthenticate() {
+ return apiKey != null || password != null;
+ }
+
@Override
public void close() throws Exception {
authToken = null;
diff --git
a/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/CyberArkVaultEndpointCredentialsTest.java
b/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/CyberArkVaultEndpointCredentialsTest.java
new file mode 100644
index 000000000000..c178256dca54
--- /dev/null
+++
b/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/CyberArkVaultEndpointCredentialsTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.cyberark.vault;
+
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Endpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * An endpoint without usable credentials fails to start, with a message that
names the options to set, instead of
+ * failing on the first message. An endpoint with an authToken uses only that
token.
+ */
+class CyberArkVaultEndpointCredentialsTest extends CamelTestSupport {
+
+ private static final String CONJUR =
"url=http://localhost:1&account=myaccount";
+
+ @Test
+ void apiKeyWithoutUsernameFailsAtStart() throws Exception {
+ Endpoint endpoint = context.getComponent("cyberark-vault")
+ .createEndpoint("cyberark-vault:secret?secretId=db/password&"
+ CONJUR + "&apiKey=my-api-key");
+
+ IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, endpoint::start);
+ assertNamesTheOptions(e);
+ assertFalse(e.getMessage().contains("my-api-key"), e.getMessage());
+ }
+
+ @Test
+ void noCredentialsFailsAtStart() {
+ CyberArkVaultConfiguration configuration = new
CyberArkVaultConfiguration();
+ configuration.setUrl("http://localhost:1");
+ configuration.setAccount("myaccount");
+ configuration.setSecretId("db/password");
+ CyberArkVaultEndpoint endpoint = new CyberArkVaultEndpoint(
+ "cyberark-vault:secret",
context.getComponent("cyberark-vault"), configuration);
+
+ IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, endpoint::start);
+ assertNamesTheOptions(e);
+ }
+
+ @Test
+ void usernameAndApiKeyStart() throws Exception {
+ CyberArkVaultEndpoint endpoint = (CyberArkVaultEndpoint)
context.getComponent("cyberark-vault")
+ .createEndpoint("cyberark-vault:secret?secretId=db/password&"
+ CONJUR
+ + "&username=host/myapp&apiKey=my-api-key");
+
+ endpoint.start();
+ try {
+ assertNotNull(endpoint.getConjurClient());
+ } finally {
+ endpoint.stop();
+ }
+ }
+
+ @Test
+ void authTokenTakesPrecedenceOverTheLoginCredentials() throws Exception {
+ AtomicInteger logins = new AtomicInteger();
+ HttpServer conjur = HttpServer.create(new
InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+ conjur.createContext("/authn/", exchange -> {
+ logins.incrementAndGet();
+ exchange.getRequestBody().readAllBytes();
+ byte[] token = "new-token".getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(200, token.length);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.write(token);
+ }
+ });
+ conjur.createContext("/secrets/", exchange -> {
+ exchange.sendResponseHeaders(401, -1);
+ exchange.close();
+ });
+ conjur.start();
+ try {
+ String uri =
"cyberark-vault:secret?secretId=db/password&url=http://localhost:" +
conjur.getAddress().getPort()
+ +
"&account=myaccount&authToken=my-token&username=host/myapp&apiKey=my-api-key";
+
+ // the endpoint uses only the token, so the 401 fails at once,
without a login with the API key
+ CamelExecutionException e =
assertThrows(CamelExecutionException.class,
+ () -> template.requestBody(uri, null, String.class));
+ String message = e.getCause().getCause().getMessage();
+ assertTrue(message.contains("HTTP 401"), message);
+ assertEquals(0, logins.get());
+ } finally {
+ conjur.stop(0);
+ }
+ }
+
+ private static void assertNamesTheOptions(IllegalArgumentException e) {
+ String message = e.getMessage();
+ assertTrue(message.contains("authToken"), message);
+ assertTrue(message.contains("username and apiKey"), message);
+ assertTrue(message.contains("username and password"), message);
+ assertTrue(message.contains("conjurClient"), message);
+ }
+}
diff --git
a/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientReauthenticationTest.java
b/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientReauthenticationTest.java
new file mode 100644
index 000000000000..6c84e2a1ea40
--- /dev/null
+++
b/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientReauthenticationTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.cyberark.vault.client.impl;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.component.cyberark.vault.client.ConjurClient;
+import org.apache.camel.component.cyberark.vault.client.ConjurClientFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A 401 from Conjur makes the client authenticate again and retry once, and
only when it has an API key or a password
+ * to get a new token. A fake Conjur answers the queued statuses to the secret
requests, and then succeeds: a client
+ * that retries more than it should gets the secret instead of the expected
failure.
+ */
+class ConjurClientReauthenticationTest {
+
+ private static final String ACCOUNT = "myaccount";
+ private static final String SECRET_ID = "db/password";
+
+ private final AtomicInteger logins = new AtomicInteger();
+ private final List<String> secretRequestTokens = new
CopyOnWriteArrayList<>();
+ private final List<String> storedValues = new CopyOnWriteArrayList<>();
+ private final Queue<Integer> secretStatuses = new
ConcurrentLinkedQueue<>();
+
+ private HttpServer server;
+ private String url;
+
+ @BeforeEach
+ void startConjur() throws IOException {
+ server = HttpServer.create(new
InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+ server.createContext("/authn/", exchange -> {
+ exchange.getRequestBody().readAllBytes();
+ respond(exchange, 200, "token-" + logins.incrementAndGet());
+ });
+ server.createContext("/secrets/", exchange -> {
+ String body = new String(exchange.getRequestBody().readAllBytes(),
StandardCharsets.UTF_8);
+ secretRequestTokens.add(tokenOf(exchange));
+ Integer status = secretStatuses.poll();
+ if ("POST".equals(exchange.getRequestMethod())) {
+ storedValues.add(body);
+ respond(exchange, status != null ? status : 201, "");
+ } else {
+ respond(exchange, status != null ? status : 200,
"secret-value");
+ }
+ });
+ server.start();
+ url = "http://localhost:" + server.getAddress().getPort();
+ }
+
+ @AfterEach
+ void stopConjur() {
+ server.stop(0);
+ }
+
+ @Test
+ void retrieveSecretWithOnlyATokenFailsWithTheUnauthorizedResponse() throws
Exception {
+ secretStatuses.add(401);
+ secretStatuses.add(401);
+
+ try (ConjurClient client = ConjurClientFactory.createWithToken(url,
ACCOUNT, "my-token")) {
+ RuntimeException e = assertThrows(RuntimeException.class, () ->
client.retrieveSecret(SECRET_ID));
+ assertTrue(e.getCause().getMessage().contains("HTTP 401"),
e.getCause().getMessage());
+ }
+ assertEquals(List.of("my-token"), secretRequestTokens);
+ assertEquals(0, logins.get());
+ }
+
+ @Test
+ void retrieveSecretAuthenticatesAgainAndRetriesOnce() throws Exception {
+ secretStatuses.add(401);
+
+ try (ConjurClient client = ConjurClientFactory.createWithApiKey(url,
ACCOUNT, "host/myapp", "my-api-key")) {
+ assertEquals("secret-value", client.retrieveSecret(SECRET_ID));
+ }
+ assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+ assertEquals(2, logins.get());
+ }
+
+ @Test
+ void retrieveSecretFailsWhenTheNewTokenIsRejectedToo() throws Exception {
+ secretStatuses.add(401);
+ secretStatuses.add(401);
+
+ try (ConjurClient client =
ConjurClientFactory.createWithCredentials(url, ACCOUNT, "admin",
"my-password")) {
+ RuntimeException e = assertThrows(RuntimeException.class, () ->
client.retrieveSecret(SECRET_ID));
+ assertTrue(e.getCause().getMessage().contains("HTTP 401"),
e.getCause().getMessage());
+ }
+ assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+ assertEquals(2, logins.get());
+ }
+
+ @Test
+ void createSecretWithOnlyATokenFailsWithTheUnauthorizedResponse() throws
Exception {
+ secretStatuses.add(401);
+ secretStatuses.add(401);
+
+ try (ConjurClient client = ConjurClientFactory.createWithToken(url,
ACCOUNT, "my-token")) {
+ RuntimeException e = assertThrows(RuntimeException.class, () ->
client.createSecret(SECRET_ID, "new-value"));
+ assertTrue(e.getCause().getMessage().contains("HTTP 401"),
e.getCause().getMessage());
+ }
+ assertEquals(List.of("my-token"), secretRequestTokens);
+ assertEquals(0, logins.get());
+ }
+
+ @Test
+ void createSecretAuthenticatesAgainAndRetriesOnce() throws Exception {
+ secretStatuses.add(401);
+
+ try (ConjurClient client = ConjurClientFactory.createWithApiKey(url,
ACCOUNT, "host/myapp", "my-api-key")) {
+ client.createSecret(SECRET_ID, "new-value");
+ }
+ assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+ assertEquals(List.of("new-value", "new-value"), storedValues);
+ assertEquals(2, logins.get());
+ }
+
+ @Test
+ void createSecretFailsWhenTheNewTokenIsRejectedToo() throws Exception {
+ secretStatuses.add(401);
+ secretStatuses.add(401);
+
+ try (ConjurClient client =
ConjurClientFactory.createWithCredentials(url, ACCOUNT, "admin",
"my-password")) {
+ RuntimeException e = assertThrows(RuntimeException.class, () ->
client.createSecret(SECRET_ID, "new-value"));
+ assertTrue(e.getCause().getMessage().contains("HTTP 401"),
e.getCause().getMessage());
+ }
+ assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+ assertEquals(2, logins.get());
+ }
+
+ private static String tokenOf(HttpExchange exchange) {
+ // Authorization: Token token="<base64 of the access token>"
+ String authorization =
exchange.getRequestHeaders().getFirst("Authorization");
+ String encoded = authorization.substring("Token token=\"".length(),
authorization.length() - 1);
+ return new String(Base64.getDecoder().decode(encoded),
StandardCharsets.UTF_8);
+ }
+
+ private static void respond(HttpExchange exchange, int status, String
body) throws IOException {
+ byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 :
bytes.length);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.write(bytes);
+ }
+ }
+}
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
index d85dce0dbc0c..c2bea3562783 100644
--- 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
@@ -553,6 +553,20 @@ A route that relies on a regular expression as token, such
as `\s+` or `(\W+)\s*
In the XML and YAML DSL, a token with the escapes `\n`, `\r` or `\t` (for
example `\r\n`) is turned into the new line,
carriage return and tab characters, so it works as before. Before, only a
leading `\n` was turned into a new line.
+=== camel-cyberark-vault - credentials are checked at endpoint start, and a
rejected token is retried only once
+
+A `cyberark-vault` endpoint now fails to start when it has no usable
credentials, with a message that names the
+options to set: the `authToken` option, the `username` and `apiKey` options,
or the `username` and `password` options,
+unless the endpoint has a `conjurClient`. Before, an endpoint with an `apiKey`
and no `username` started, and its first
+message failed with a `NullPointerException`.
+
+When CyberArk Conjur answers HTTP 401, the endpoint and the CyberArk Conjur
Vault Property Function authenticate again
+and retry the call once, and only when they have an `apiKey` or a `password`
to authenticate with. With only an
+`authToken`, the call now fails with the 401 response, instead of retrying
with the same token until a
+`StackOverflowError`. Configure the `username` with an `apiKey` or a
`password`, instead of an `authToken`, to let the
+component get a new token when the current one expires: when the `authToken`
is set, the component uses only that
+token.
+
=== camel-main - a failure to start is logged
A Camel Main application that fails to start now logs why, at ERROR: `Error
starting Camel: ...` with the stack trace,