You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2026/09/08 08:11:57 UTC

(camel) branch main updated: CAMEL-24636: camel-core - re-authenticate components on context reload after a secret rotation (#26166)

This is an automated email from the ASF dual-hosted git repository.

oscerd 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 0b46030091bb CAMEL-24636: camel-core - re-authenticate components on context reload after a secret rotation (#26166)
0b46030091bb is described below

commit 0b46030091bbfebd808564643155a40e5d95da22
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Tue Sep 8 10:11:47 2026 +0200

    CAMEL-24636: camel-core - re-authenticate components on context reload after a secret rotation (#26166)
    
    The vault components detect a rotated secret and trigger a context reload, but
    the reload stopped short of the components holding the authenticated connection:
    it restarted the properties sources and reloaded the routes, leaving components
    and registry beans untouched.
    
    Re-apply the camel. options whose value is a property placeholder, so the newly
    resolved secret reaches the component. MainPropertiesReload already did this, but
    was only invoked from the file-watch strategies, and the raw placeholder is not
    retained on the component, so a component could not have re-resolved it on its own.
    
    Add the SecretRotationAware SPI so a component or registry bean holding a live
    authenticated resource can rebuild it. The callback runs after the options are
    re-applied and before the routes restart, and a callback that throws is logged
    and skipped so one component cannot break the reload.
    
    Co-authored-by: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 .../org/apache/camel/spi/SecretRotationAware.java  |  57 +++++
 .../impl/CamelContextSecretRotationAwareTest.java  | 250 +++++++++++++++++++++
 .../impl/ContextReloadComponentPropertiesTest.java | 123 ++++++++++
 .../main/MainContextReloadSecretRotationTest.java  | 113 ++++++++++
 .../support/DefaultContextReloadStrategy.java      |  77 +++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  19 ++
 .../modules/ROOT/pages/context-reload.adoc         |  52 ++++-
 7 files changed, 687 insertions(+), 4 deletions(-)

diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/SecretRotationAware.java b/core/camel-api/src/main/java/org/apache/camel/spi/SecretRotationAware.java
new file mode 100644
index 000000000000..dd1e154094b6
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/SecretRotationAware.java
@@ -0,0 +1,57 @@
+/*
+ * 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.spi;
+
+/**
+ * SPI for components and beans that capture a secret when they are configured, and that therefore need to be told when
+ * that secret has been rotated so they can re-authenticate in place.
+ * <p/>
+ * When a vault component detects that a secret changed it triggers a {@link ContextReloadStrategy}, which reloads the
+ * property placeholders and then all routes. Routes and endpoints are rebuilt from scratch, but components and beans in
+ * the {@link Registry} are not, so anything holding a live authenticated resource - a pooled JMS connection factory, a
+ * JDBC connection pool, a shared HTTP client - keeps using the credentials it captured at startup.
+ * <p/>
+ * Implement this interface on a {@link org.apache.camel.Component}, or register a bean implementing it in the
+ * {@link Registry}, to be notified when this happens. The callback runs after the property placeholders have been
+ * reloaded and the component options re-applied, but before the routes are restarted, so that by the time the routes
+ * come back up the underlying resource is already authenticated with the new secret.
+ * <p/>
+ * Implementations should re-establish the authenticated resource rather than assume the process will be restarted, and
+ * should be quick: the callback runs inline on the reload, and every implementation is notified before the routes come
+ * back. A callback that throws is logged and ignored, so that one component cannot prevent the others from being
+ * refreshed, nor break the reload as a whole.
+ *
+ * @see   ContextReloadStrategy
+ * @since 4.23
+ */
+@FunctionalInterface
+public interface SecretRotationAware {
+
+    /**
+     * Callback invoked when the secrets this component or bean captured may have been rotated, and it should
+     * re-authenticate in place.
+     * <p/>
+     * The callback is advisory: it says that a reload was triggered, not which secrets changed. Implementations should
+     * re-read their configuration and refresh the authenticated resource if the credentials it holds are no longer the
+     * configured ones.
+     *
+     * @param  source    the source that triggered the reload, such as the vault task that detected the change
+     * @throws Exception is thrown if the resource could not be re-authenticated
+     */
+    void onSecretRotation(Object source) throws Exception;
+
+}
diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextSecretRotationAwareTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextSecretRotationAwareTest.java
new file mode 100644
index 000000000000..732d70d5b1b2
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextSecretRotationAwareTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Endpoint;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesSource;
+import org.apache.camel.spi.SecretRotationAware;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that {@link SecretRotationAware} components and beans are notified when the context is reloaded, so they can
+ * re-authenticate with a rotated secret before the routes are restarted.
+ */
+public class CamelContextSecretRotationAwareTest extends ContextTestSupport {
+
+    private final MyRotationAware bean = new MyRotationAware();
+    private final FailingRotationAware failing = new FailingRotationAware();
+    private final MyRotationAwareComponent component = new MyRotationAwareComponent();
+    private final MyRotationAwareComponent sharedComponent = new MyRotationAwareComponent();
+    private final List<String> order = new ArrayList<>();
+
+    @Test
+    public void testRegistryBeanIsNotified() {
+        assertThat(bean.getCounter()).isZero();
+
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        assertThat(crs).isNotNull();
+        crs.onReload("CamelContextSecretRotationAwareTest");
+
+        assertThat(bean.getCounter()).isOne();
+        assertThat(bean.getLastSource()).isEqualTo("CamelContextSecretRotationAwareTest");
+    }
+
+    @Test
+    public void testComponentIsNotified() {
+        assertThat(component.getCounter()).isZero();
+
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        crs.onReload("CamelContextSecretRotationAwareTest");
+
+        // a component is the primary adopter of the SPI, as it is what holds the authenticated connection
+        assertThat(component.getCounter()).isOne();
+        assertThat(component.getLastSource()).isEqualTo("CamelContextSecretRotationAwareTest");
+    }
+
+    @Test
+    public void testComponentAlsoInRegistryIsNotifiedOnce() {
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        crs.onReload("CamelContextSecretRotationAwareTest");
+
+        // the same instance is both an added component and a registry bean, as happens on Spring Boot,
+        // and must not be notified twice
+        assertThat(sharedComponent.getCounter()).isOne();
+    }
+
+    @Test
+    public void testNotifiedOnEveryReload() {
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        crs.onReload("first");
+        crs.onReload("second");
+
+        assertThat(bean.getCounter()).isEqualTo(2);
+        assertThat(component.getCounter()).isEqualTo(2);
+        assertThat(bean.getLastSource()).isEqualTo("second");
+    }
+
+    @Test
+    public void testFailingListenerDoesNotBreakTheReload() {
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        crs.onReload("boom");
+
+        // the failing listener was invoked, but the reload still succeeded and the others were notified
+        assertThat(failing.getCounter()).isOne();
+        assertThat(bean.getCounter()).isOne();
+        assertThat(component.getCounter()).isOne();
+        assertThat(crs.getLastError()).isNull();
+        assertThat(context.getRoutes()).hasSize(1);
+    }
+
+    @Test
+    public void testNotifiedBeforeRoutesAreReloaded() {
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        crs.onReload("ordering");
+
+        // the secret must be refreshed before the routes come back up, otherwise the new consumers
+        // would be created with the credentials that were just rotated away
+        assertThat(order).containsExactly("secret-rotation", "route-reload");
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+
+        PropertiesComponent pc = context.getPropertiesComponent();
+        MySource my = new MySource();
+        ServiceHelper.startService(my);
+        pc.addPropertiesSource(my);
+
+        context.getRegistry().bind("myRotationAware", bean);
+        context.getRegistry().bind("failingRotationAware", failing);
+
+        context.addComponent("myrotate", component);
+        // the shared component is reachable both as a component and as a registry bean
+        context.addComponent("mysharedrotate", sharedComponent);
+        context.getRegistry().bind("mySharedRotateComponent", sharedComponent);
+
+        ContextReloadStrategy crs = new DefaultContextReloadStrategy() {
+            @Override
+            protected void notifySecretRotation(Object source) {
+                order.add("secret-rotation");
+                super.notifySecretRotation(source);
+            }
+
+            @Override
+            protected void reloadRoutes(Object source) throws Exception {
+                order.add("route-reload");
+                super.reloadRoutes(source);
+            }
+        };
+        context.addService(crs);
+
+        return context;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setBody(constant("{{hello}}"))
+                        .to("mock:result");
+            }
+        };
+    }
+
+    private static class MyRotationAware implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+        private volatile String lastSource;
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+            lastSource = source != null ? source.toString() : null;
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+
+        String getLastSource() {
+            return lastSource;
+        }
+    }
+
+    private static class FailingRotationAware implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+            throw new IllegalStateException("Cannot re-authenticate");
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+    }
+
+    private static class MyRotationAwareComponent extends DefaultComponent implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+        private volatile String lastSource;
+
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+            lastSource = source != null ? source.toString() : null;
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+
+        String getLastSource() {
+            return lastSource;
+        }
+    }
+
+    private static class MySource extends ServiceSupport implements PropertiesSource {
+
+        private int counter;
+
+        @Override
+        public String getName() {
+            return "my";
+        }
+
+        @Override
+        public String getProperty(String name) {
+            if ("hello".equals(name)) {
+                return "Hello " + counter;
+            }
+            return null;
+        }
+
+        @Override
+        protected void doStart() {
+            // the properties source will be restarted
+            counter++;
+        }
+    }
+}
diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/ContextReloadComponentPropertiesTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/ContextReloadComponentPropertiesTest.java
new file mode 100644
index 000000000000..366a86e6de07
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/impl/ContextReloadComponentPropertiesTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.impl;
+
+import java.util.Properties;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.NonManagedService;
+import org.apache.camel.StaticService;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesReload;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.OrderedLocationProperties;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that a context reload re-applies the Camel options whose value is a property placeholder, which is what allows
+ * a rotated secret to reach a component option that was resolved once at bootstrap.
+ */
+public class ContextReloadComponentPropertiesTest extends ContextTestSupport {
+
+    private final MyPropertiesReload reload = new MyPropertiesReload();
+
+    @Test
+    public void testPlaceholderOptionsAreReApplied() {
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        assertThat(crs).isNotNull();
+        crs.onReload("ContextReloadComponentPropertiesTest");
+
+        assertThat(reload.getCounter()).isOne();
+        Properties reloaded = reload.getProperties();
+        assertThat(reloaded).isNotNull();
+
+        // only camel options whose value is a placeholder are re-applied, as they are the only ones
+        // whose resolved value can change while the raw configuration stays the same
+        assertThat(reloaded.stringPropertyNames())
+                .containsExactlyInAnyOrder("camel.component.dummy.secret", "camel.component.dummy.token");
+        assertThat(reloaded.getProperty("camel.component.dummy.secret")).isEqualTo("{{hello}}");
+    }
+
+    @Test
+    public void testPropertiesAreOrderedLocationProperties() {
+        ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+        crs.onReload("ContextReloadComponentPropertiesTest");
+
+        // MainPropertiesReload only acts on OrderedLocationProperties, so the contract must be kept
+        assertThat(reload.getProperties()).isInstanceOf(OrderedLocationProperties.class);
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+
+        Properties prop = new Properties();
+        prop.setProperty("hello", "Hello World");
+        // placeholder based camel options: must be re-applied
+        prop.setProperty("camel.component.dummy.secret", "{{hello}}");
+        prop.setProperty("camel.component.dummy.token", "prefix-{{hello}}-suffix");
+        // plain camel option: cannot change, must be filtered out
+        prop.setProperty("camel.component.dummy.plain", "no-placeholder");
+        // placeholder, but not a camel option: not Camel configuration, must be filtered out
+        prop.setProperty("myapp.password", "{{hello}}");
+
+        PropertiesComponent pc = context.getPropertiesComponent();
+        pc.setInitialProperties(prop);
+
+        context.addService(reload);
+        context.addService(new DefaultContextReloadStrategy());
+
+        return context;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").to("mock:result");
+            }
+        };
+    }
+
+    private static class MyPropertiesReload extends ServiceSupport
+            implements PropertiesReload, StaticService, NonManagedService {
+
+        private int counter;
+        private Properties properties;
+
+        @Override
+        public void onReload(String name, Properties properties) {
+            this.counter++;
+            this.properties = properties;
+        }
+
+        int getCounter() {
+            return counter;
+        }
+
+        Properties getProperties() {
+            return properties;
+        }
+    }
+}
diff --git a/core/camel-main/src/test/java/org/apache/camel/main/MainContextReloadSecretRotationTest.java b/core/camel-main/src/test/java/org/apache/camel/main/MainContextReloadSecretRotationTest.java
new file mode 100644
index 000000000000..a0f6c55a86aa
--- /dev/null
+++ b/core/camel-main/src/test/java/org/apache/camel/main/MainContextReloadSecretRotationTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.main;
+
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.main.support.MyDummyComponent;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spi.SecretRotationAware;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * End-to-end test of what a vault component triggers when it detects a rotated secret: the component option that was
+ * configured with a placeholder is re-resolved, and {@link SecretRotationAware} beans are told to re-authenticate.
+ */
+public class MainContextReloadSecretRotationTest {
+
+    @Test
+    public void testRotatedSecretReachesComponentOption() {
+        MyVaultFunction vault = new MyVaultFunction();
+
+        Properties properties = new Properties();
+        properties.setProperty("camel.main.context-reload-enabled", "true");
+        properties.setProperty("camel.component.dummy.component-value", "{{vault:password}}");
+
+        Main main = new Main();
+        MyRotationAware bean = new MyRotationAware();
+        try {
+            main.bind("dummy", new MyDummyComponent(false));
+            main.bind("myRotationAware", bean);
+            main.setOverrideProperties(properties);
+            main.setDefaultPropertyPlaceholderLocation("false");
+            main.addMainListener(new MainListenerSupport() {
+                @Override
+                public void beforeConfigure(BaseMainSupport main) {
+                    main.getCamelContext().getPropertiesComponent().addPropertiesFunction(vault);
+                }
+            });
+            main.start();
+
+            CamelContext context = main.getCamelContext();
+            MyDummyComponent dummy = (MyDummyComponent) context.getComponent("dummy");
+            assertThat(dummy.getComponentValue()).isEqualTo("password-1");
+            assertThat(bean.getCounter()).isZero();
+
+            // the secret is rotated in the vault, and the vault component triggers a context reload
+            vault.rotate();
+            ContextReloadStrategy crs = context.hasService(ContextReloadStrategy.class);
+            assertThat(crs).isNotNull();
+            crs.onReload("MainContextReloadSecretRotationTest");
+
+            // the component option now holds the rotated secret, and the bean was told to re-authenticate
+            MyDummyComponent reloaded = (MyDummyComponent) context.getComponent("dummy");
+            assertThat(reloaded.getComponentValue()).isEqualTo("password-2");
+            assertThat(bean.getCounter()).isOne();
+            assertThat(crs.getLastError()).isNull();
+        } finally {
+            main.stop();
+        }
+    }
+
+    private static class MyVaultFunction implements PropertiesFunction {
+
+        private int counter = 1;
+
+        void rotate() {
+            counter++;
+        }
+
+        @Override
+        public String getName() {
+            return "vault";
+        }
+
+        @Override
+        public String apply(String remainder) {
+            return remainder + "-" + counter;
+        }
+    }
+
+    private static class MyRotationAware implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+    }
+}
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java b/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java
index f4590e8df6fb..58ee0f65bfe8 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java
@@ -16,13 +16,20 @@
  */
 package org.apache.camel.support;
 
+import java.util.LinkedHashSet;
+import java.util.Properties;
+import java.util.Set;
+
 import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
 import org.apache.camel.api.management.ManagedAttribute;
 import org.apache.camel.api.management.ManagedOperation;
 import org.apache.camel.api.management.ManagedResource;
 import org.apache.camel.spi.ContextReloadStrategy;
 import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesReload;
 import org.apache.camel.spi.PropertiesSource;
+import org.apache.camel.spi.SecretRotationAware;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.service.ServiceSupport;
 import org.slf4j.Logger;
@@ -63,6 +70,11 @@ public class DefaultContextReloadStrategy extends ServiceSupport implements Cont
             lastError = null;
             EventHelper.notifyContextReloading(getCamelContext(), source);
             reloadProperties(source);
+            // the order matters: the components must hold the newly resolved secrets before they are asked
+            // to re-authenticate, and both must happen before the routes come back up, so that the new
+            // consumers and producers are created against an already re-authenticated resource
+            reloadComponentProperties(source);
+            notifySecretRotation(source);
             reloadRoutes(source);
             incSucceededCounter();
             EventHelper.notifyContextReloaded(getCamelContext(), source);
@@ -87,6 +99,71 @@ public class DefaultContextReloadStrategy extends ServiceSupport implements Cont
         }
     }
 
+    /**
+     * Re-applies the configuration properties whose value is a property placeholder, so that components are
+     * re-configured with what those placeholders resolve to now.
+     * <p/>
+     * A component option such as <tt>camel.component.kafka.saslJaasConfig</tt> has its placeholder resolved once, when
+     * the component is configured, and the resolved value is what is stored on the component. Reloading the routes
+     * rebuilds the endpoints from that same already-resolved value, so without this step a rotated secret would never
+     * reach the component. Only <tt>camel.</tt> options whose value is a placeholder are handed to the listener, as
+     * they are the only ones whose resolved value can change while the raw configuration stays the same.
+     */
+    protected void reloadComponentProperties(Object source) throws Exception {
+        PropertiesReload pr = getCamelContext().hasService(PropertiesReload.class);
+        if (pr == null) {
+            // component re-configuration is only supported when running with Camel Main
+            return;
+        }
+
+        PropertiesComponent pc = getCamelContext().getPropertiesComponent();
+        Properties prop = pc.loadProperties();
+        // filter on camel. rather than on the individual option prefixes: PropertiesReload is a generic SPI and
+        // each implementation decides which options it acts on. MainPropertiesReload, for example, re-applies only
+        // camel.component., camel.dataformat. and camel.language., and silently ignores everything else
+        // stringPropertyNames is a live view of the keys, so snapshot before removing
+        Set<String> keys = new LinkedHashSet<>(prop.stringPropertyNames());
+        for (String key : keys) {
+            Object value = prop.get(key);
+            boolean placeholder = key.startsWith("camel.")
+                    && value instanceof String str && str.contains(PropertiesComponent.PREFIX_TOKEN);
+            if (!placeholder) {
+                prop.remove(key);
+            }
+        }
+        if (!prop.isEmpty()) {
+            LOG.debug("Re-applying {} property placeholder based options to components", prop.size());
+            pr.onReload(source != null ? source.toString() : "ContextReload", prop);
+        }
+    }
+
+    /**
+     * Notifies every {@link SecretRotationAware} component and registry bean that the secrets they captured may have
+     * been rotated, so they can re-authenticate before the routes are restarted.
+     * <p/>
+     * A listener that throws is logged and skipped, so that one component cannot prevent the others from being
+     * refreshed, nor fail the reload as a whole.
+     */
+    protected void notifySecretRotation(Object source) {
+        Set<SecretRotationAware> targets = new LinkedHashSet<>();
+        for (String name : getCamelContext().getComponentNames()) {
+            Component component = getCamelContext().hasComponent(name);
+            if (component instanceof SecretRotationAware sra) {
+                targets.add(sra);
+            }
+        }
+        targets.addAll(getCamelContext().getRegistry().findByType(SecretRotationAware.class));
+
+        for (SecretRotationAware target : targets) {
+            try {
+                target.onSecretRotation(source);
+            } catch (Exception e) {
+                LOG.warn("Error re-authenticating {} after secret rotation due to: {}. This exception is ignored.",
+                        target, e.getMessage(), e);
+            }
+        }
+    }
+
     @ManagedAttribute(description = "Number of reloads succeeded")
     public int getReloadCounter() {
         return succeeded;
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 09de0d03b17f..3f3927dac2d6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -13,6 +13,25 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details.
 
 == Upgrading Camel 4.22 to 4.23
 
+=== Context reload now re-applies placeholder based component options
+
+When a context reload is triggered, for example by one of the vault components detecting that a secret was rotated,
+Camel now also re-applies the `camel.component.`, `camel.dataformat.` and `camel.language.` options whose configured
+value is a property placeholder, and notifies any bean implementing the new
+`org.apache.camel.spi.SecretRotationAware` SPI. Previously only the property placeholders and the routes were
+reloaded, so a rotated secret never reached a component option that had been resolved at bootstrap.
+
+Re-applying such an option removes the component and resolves it again, as it already does when the option changes
+in a watched properties file. The component is stopped either way, and what comes back depends on how it was
+registered: a component auto-detected from the classpath is replaced by a new instance, while a component registered
+as a bean is the same instance, re-configured and started again. A component that holds a connection will therefore
+close it on reload.
+
+Only options whose value is a placeholder are affected, and only when running with Camel Main, Camel Spring Boot or
+Camel Quarkus. If a component must not be stopped on reload, configure it programmatically rather than with a
+placeholder based property.
+
+
 === Apache Avro trusted packages
 
 Camel now uses Apache Avro 1.12.2. Avro validates classes resolved from schemas
diff --git a/docs/user-manual/modules/ROOT/pages/context-reload.adoc b/docs/user-manual/modules/ROOT/pages/context-reload.adoc
index 67475a748959..6a48f16f5793 100644
--- a/docs/user-manual/modules/ROOT/pages/context-reload.adoc
+++ b/docs/user-manual/modules/ROOT/pages/context-reload.adoc
@@ -6,12 +6,27 @@ upon an external triggered event.
 For example, if you are using xref:components::aws-secrets-manager-component.adoc[AWS Secrets], then
 enabling context-reload would then reload Camel routes upon a secret is updated in AWS.
 
-The context reload is limited to refresh the following on reload:
+The context reload refreshes the following on reload:
 
 - xref:using-propertyplaceholder.adoc[property placeholders]
+- component options whose configured value is a property placeholder (requires Camel Main, Camel Spring Boot or
+  Camel Quarkus)
+- all beans implementing `SecretRotationAware`, so they can re-authenticate in place (see below)
 - all existing xref:routes.adoc[routes] (no changes to structure of routes; see xref:route-reload.adoc[]])
 
-General services in xref:camelcontext.adoc[CamelContext] and java beans or Camel xref:processor.adoc[] is not updated.
+Other general services in xref:camelcontext.adoc[CamelContext] and java beans or Camel xref:processor.adoc[] are not
+updated.
+
+Re-applying the component options is what allows a rotated secret to reach a component, as an option such as the
+following has its placeholder resolved once, when the component is configured:
+
+[source,properties]
+----
+camel.component.kafka.saslJaasConfig = {{aws:broker-credentials}}
+----
+
+Only options whose value is a placeholder are re-applied, as they are the only ones whose resolved value can change
+while the configuration itself stays the same.
 
 == Using context reloading
 
@@ -47,9 +62,38 @@ if (reload != null) {
 }
 ----
 
-The method `onReload` will then reload all the xref:using-propertyplaceholder.adoc[property placeholders] and
-then afterward reload all existing xref:routes.adoc[routes].
+The method `onReload` will then reload all the xref:using-propertyplaceholder.adoc[property placeholders],
+re-apply the component options that were configured with a placeholder, notify all `SecretRotationAware` beans,
+and then afterward reload all existing xref:routes.adoc[routes].
+
+== Re-authenticating on rotated secrets
+
+Reloading the routes rebuilds the routes and their endpoints, but it does not rebuild the components themselves, nor
+any bean in the xref:registry.adoc[Registry]. A component that holds a live authenticated resource, such as a pooled
+JMS connection factory, a JDBC connection pool or a shared HTTP client, therefore keeps using the credentials it
+captured when it was created, even though the secret has been rotated.
+
+Such a component or bean can implement `org.apache.camel.spi.SecretRotationAware` to be told when this happens:
+
+[source,java]
+----
+public class MyComponent extends DefaultComponent implements SecretRotationAware {
+
+    @Override
+    public void onSecretRotation(Object source) throws Exception {
+        // re-read the configured credentials and re-authenticate the connection
+        // that was created with the secret that has just been rotated
+    }
+}
+----
+
+The callback is invoked after the property placeholders have been reloaded and the component options re-applied, but
+before the routes are restarted, so that by the time the routes come back up the underlying resource is already
+authenticated with the new secret.
 
+The callback is advisory: it says that a reload was triggered, not which secrets changed. Implementations should be
+quick, as the callback runs inline on the reload. A callback that throws is logged and ignored, so that one component
+cannot prevent the others from being refreshed, nor fail the reload as a whole.
 
 == See Also