Introduce configuration properties for session lifecycle management
We now provide configuration properties to configure session token renewal, especially renewal and expiry thresholds.
spring.cloud.vault:
session.lifecycle:
enabled: true
refresh-before-expiry: 10s
expiry-threshold: 20s
Closes gh-400.
This commit is contained in:
@@ -1310,3 +1310,44 @@ A lease is renewed the configured period of time before it expires.
|
||||
Legacy for vault versions before 0.8 and SysLeases for later.
|
||||
|
||||
See also: https://www.vaultproject.io/docs/concepts/lease.html[Vault Documentation: Lease, Renew, and Revoke]
|
||||
|
||||
[[vault-session-lifecycle]]
|
||||
== Session token lifecycle management (renewal, re-login and revocation)
|
||||
|
||||
A Vault session token (also referred to as `LoginToken`) is quite similar to a lease as it has a TTL, max TTL, and may expire.
|
||||
Once a login token expires, it cannot be used anymore to interact with Vault.
|
||||
Therefore, Spring Vault ships with a `SessionManager` API for imperative and reactive use.
|
||||
|
||||
Spring Cloud Vault maintains the session token lifecycle by default.
|
||||
Session tokens are obtained lazily so the actual login is deferred until the first session-bound use of Vault.
|
||||
Once Spring Cloud Vault obtains a session token, it retains it until expiry.
|
||||
The next time a session-bound activity is used, Spring Cloud Vault re-logins into Vault and obtains a new session token.
|
||||
On application shut down, Spring Cloud Vault revokes the token if it was still active to terminate the session.
|
||||
|
||||
Session lifecycle is enabled by default and can be disabled by setting `spring.cloud.vault.session.lifecycle.enabled`
|
||||
to `false`.
|
||||
Disabling is not recommended as session tokens can expire and Spring Cloud Vault cannot longer access Vault.
|
||||
|
||||
====
|
||||
[source,yaml]
|
||||
----
|
||||
spring.cloud.vault:
|
||||
session.lifecycle:
|
||||
enabled: true
|
||||
refresh-before-expiry: 10s
|
||||
expiry-threshold: 20s
|
||||
----
|
||||
====
|
||||
|
||||
* `enabled` controls whether session lifecycle management is enabled to renew session tokens.
|
||||
Enabled by default.
|
||||
* `refresh-before-expiry` controls the point in time when the session token gets renewed.
|
||||
The refresh time is calculated by subtracting `refresh-before-expiry` from the token expiry time.
|
||||
Defaults to `5 seconds`.
|
||||
* `expiry-threshold` sets the expiry threshold.
|
||||
The threshold represents a minimum TTL duration to consider a session token as valid.
|
||||
Tokens with a shorter TTL are considered expired and are not used anymore.
|
||||
Should be greater than `refresh-before-expiry` to prevent token expiry.
|
||||
Defaults to `7 seconds`.
|
||||
|
||||
See also: https://www.vaultproject.io/api-docs/auth/token#renew-a-token-self[Vault Documentation: Token Renewal]
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.authentication.ClientAuthentication;
|
||||
import org.springframework.vault.authentication.LifecycleAwareSessionManager;
|
||||
import org.springframework.vault.authentication.LifecycleAwareSessionManagerSupport;
|
||||
import org.springframework.vault.authentication.SessionManager;
|
||||
import org.springframework.vault.authentication.SimpleSessionManager;
|
||||
import org.springframework.vault.client.ClientHttpRequestFactoryFactory;
|
||||
@@ -220,11 +221,16 @@ public class VaultBootstrapConfiguration implements InitializingBean {
|
||||
public SessionManager vaultSessionManager(ClientAuthentication clientAuthentication,
|
||||
ObjectFactory<TaskSchedulerWrapper> asyncTaskExecutorFactory) {
|
||||
|
||||
if (this.vaultProperties.getConfig().getLifecycle().isEnabled()) {
|
||||
VaultProperties.SessionLifecycle lifecycle = this.vaultProperties.getSession()
|
||||
.getLifecycle();
|
||||
|
||||
if (lifecycle.isEnabled()) {
|
||||
RestTemplate restTemplate = this.restTemplateBuilder.build();
|
||||
LifecycleAwareSessionManagerSupport.RefreshTrigger trigger = new LifecycleAwareSessionManagerSupport.FixedTimeoutRefreshTrigger(
|
||||
lifecycle.getRefreshBeforeExpiry(), lifecycle.getExpiryThreshold());
|
||||
return new LifecycleAwareSessionManager(clientAuthentication,
|
||||
asyncTaskExecutorFactory.getObject().getTaskScheduler(),
|
||||
restTemplate);
|
||||
asyncTaskExecutorFactory.getObject().getTaskScheduler(), restTemplate,
|
||||
trigger);
|
||||
}
|
||||
|
||||
return new SimpleSessionManager(clientAuthentication);
|
||||
|
||||
@@ -87,7 +87,8 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe
|
||||
PropertySourceLocatorConfiguration configuration = getPropertySourceConfiguration(
|
||||
Collections.singletonList(kvBackendProperties));
|
||||
|
||||
VaultProperties.Lifecycle lifecycle = vaultProperties.getConfig().getLifecycle();
|
||||
VaultProperties.ConfigLifecycle lifecycle = vaultProperties.getConfig()
|
||||
.getLifecycle();
|
||||
|
||||
if (lifecycle.isEnabled()) {
|
||||
|
||||
@@ -182,7 +183,8 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe
|
||||
public SecretLeaseContainer secretLeaseContainer(VaultProperties vaultProperties,
|
||||
VaultOperations vaultOperations, TaskSchedulerWrapper taskSchedulerWrapper) {
|
||||
|
||||
VaultProperties.Lifecycle lifecycle = vaultProperties.getConfig().getLifecycle();
|
||||
VaultProperties.ConfigLifecycle lifecycle = vaultProperties.getConfig()
|
||||
.getLifecycle();
|
||||
|
||||
SecretLeaseContainer container = new SecretLeaseContainer(vaultOperations,
|
||||
taskSchedulerWrapper.getTaskScheduler());
|
||||
@@ -192,7 +194,7 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe
|
||||
return container;
|
||||
}
|
||||
|
||||
static void customizeContainer(VaultProperties.Lifecycle lifecycle,
|
||||
static void customizeContainer(VaultProperties.ConfigLifecycle lifecycle,
|
||||
SecretLeaseContainer container) {
|
||||
|
||||
if (lifecycle.isEnabled()) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.vault.authentication.LoginToken;
|
||||
import org.springframework.vault.core.lease.LeaseEndpoints;
|
||||
|
||||
/**
|
||||
@@ -122,6 +123,8 @@ public class VaultProperties implements EnvironmentAware {
|
||||
|
||||
private Config config = new Config();
|
||||
|
||||
private Session session = new Session();
|
||||
|
||||
/**
|
||||
* Application name for AppId authentication.
|
||||
*/
|
||||
@@ -227,6 +230,10 @@ public class VaultProperties implements EnvironmentAware {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public Session getSession() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
public String getApplicationName() {
|
||||
return this.applicationName;
|
||||
}
|
||||
@@ -323,6 +330,10 @@ public class VaultProperties implements EnvironmentAware {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void setSession(Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public void setApplicationName(String applicationName) {
|
||||
this.applicationName = applicationName;
|
||||
}
|
||||
@@ -1025,13 +1036,13 @@ public class VaultProperties implements EnvironmentAware {
|
||||
*/
|
||||
private int order = 0;
|
||||
|
||||
private Lifecycle lifecycle = new Lifecycle();
|
||||
private ConfigLifecycle lifecycle = new ConfigLifecycle();
|
||||
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
public Lifecycle getLifecycle() {
|
||||
public ConfigLifecycle getLifecycle() {
|
||||
return this.lifecycle;
|
||||
}
|
||||
|
||||
@@ -1039,7 +1050,7 @@ public class VaultProperties implements EnvironmentAware {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public void setLifecycle(Lifecycle lifecycle) {
|
||||
public void setLifecycle(ConfigLifecycle lifecycle) {
|
||||
this.lifecycle = lifecycle;
|
||||
}
|
||||
|
||||
@@ -1049,7 +1060,7 @@ public class VaultProperties implements EnvironmentAware {
|
||||
* Configuration to Vault lifecycle management (renewal, revocation of tokens and
|
||||
* secrets).
|
||||
*/
|
||||
public static class Lifecycle {
|
||||
public static class ConfigLifecycle {
|
||||
|
||||
/**
|
||||
* Enable lifecycle management.
|
||||
@@ -1117,4 +1128,75 @@ public class VaultProperties implements EnvironmentAware {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Session management configuration properties.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class Session {
|
||||
|
||||
private SessionLifecycle lifecycle = new SessionLifecycle();
|
||||
|
||||
public SessionLifecycle getLifecycle() {
|
||||
return this.lifecycle;
|
||||
}
|
||||
|
||||
public void setLifecycle(SessionLifecycle lifecycle) {
|
||||
this.lifecycle = lifecycle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration to Vault Session lifecycle management.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class SessionLifecycle {
|
||||
|
||||
/**
|
||||
* Enable session lifecycle management.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* The time period that is at least required before renewing the
|
||||
* {@link LoginToken}.
|
||||
*/
|
||||
private Duration refreshBeforeExpiry = Duration.ofSeconds(5);
|
||||
|
||||
/**
|
||||
* The expiry threshold for a {@link LoginToken}. The threshold represents a
|
||||
* minimum TTL duration to consider a login token as valid. Tokens with a shorter
|
||||
* TTL are considered expired and are not used anymore. Should be greater than
|
||||
* {@code refreshBeforeExpiry} to prevent token expiry.
|
||||
*/
|
||||
private Duration expiryThreshold = Duration.ofSeconds(7);
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Duration getRefreshBeforeExpiry() {
|
||||
return this.refreshBeforeExpiry;
|
||||
}
|
||||
|
||||
public void setRefreshBeforeExpiry(Duration refreshBeforeExpiry) {
|
||||
this.refreshBeforeExpiry = refreshBeforeExpiry;
|
||||
}
|
||||
|
||||
public Duration getExpiryThreshold() {
|
||||
return this.expiryThreshold;
|
||||
}
|
||||
|
||||
public void setExpiryThreshold(Duration expiryThreshold) {
|
||||
this.expiryThreshold = expiryThreshold;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -179,11 +179,15 @@ public class VaultReactiveBootstrapConfiguration {
|
||||
VaultTokenSupplier vaultTokenSupplier = beanFactory.getBean("vaultTokenSupplier",
|
||||
VaultTokenSupplier.class);
|
||||
|
||||
if (this.vaultProperties.getConfig().getLifecycle().isEnabled()) {
|
||||
|
||||
VaultProperties.SessionLifecycle lifecycle = this.vaultProperties.getSession()
|
||||
.getLifecycle();
|
||||
if (lifecycle.isEnabled()) {
|
||||
WebClient webClient = this.webClientBuilder.build();
|
||||
ReactiveLifecycleAwareSessionManager.RefreshTrigger trigger = new ReactiveLifecycleAwareSessionManager.FixedTimeoutRefreshTrigger(
|
||||
lifecycle.getRefreshBeforeExpiry(), lifecycle.getExpiryThreshold());
|
||||
return new ReactiveLifecycleAwareSessionManager(vaultTokenSupplier,
|
||||
asyncTaskExecutorFactory.getObject().getTaskScheduler(), webClient);
|
||||
asyncTaskExecutorFactory.getObject().getTaskScheduler(), webClient,
|
||||
trigger);
|
||||
}
|
||||
|
||||
return CachingVaultTokenSupplier.of(vaultTokenSupplier);
|
||||
|
||||
@@ -54,6 +54,7 @@ public class ApplicationFailFastTests {
|
||||
new SpringApplicationBuilder().sources(ApplicationFailFastTests.class).run(
|
||||
"--server.port=0", "--spring.cloud.vault.failFast=true",
|
||||
"--spring.cloud.vault.config.lifecycle.enabled=false",
|
||||
"--spring.cloud.vault.session.lifecycle.enabled=false",
|
||||
"--spring.cloud.vault.port=9999");
|
||||
fail("failFast option did not produce an exception");
|
||||
}
|
||||
|
||||
@@ -16,12 +16,16 @@
|
||||
|
||||
package org.springframework.cloud.vault.config;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.vault.authentication.ClientAuthentication;
|
||||
import org.springframework.vault.authentication.SessionManager;
|
||||
import org.springframework.vault.authentication.SimpleSessionManager;
|
||||
import org.springframework.vault.core.VaultTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -48,4 +52,39 @@ public class VaultBootstrapConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDisableSessionManagement() {
|
||||
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.cloud.vault.kv.enabled=false",
|
||||
"spring.cloud.vault.token=foo",
|
||||
"spring.cloud.vault.session.lifecycle.enabled=false")
|
||||
.run(context -> {
|
||||
|
||||
SessionManager bean = context.getBean(SessionManager.class);
|
||||
assertThat(bean).isExactlyInstanceOf(SimpleSessionManager.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConfigureSessionManagement() {
|
||||
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.cloud.vault.kv.enabled=false",
|
||||
"spring.cloud.vault.token=foo",
|
||||
"spring.cloud.vault.session.lifecycle.refresh-before-expiry=11s",
|
||||
"spring.cloud.vault.session.lifecycle.expiry-threshold=12s")
|
||||
.run(context -> {
|
||||
|
||||
SessionManager bean = context.getBean(SessionManager.class);
|
||||
|
||||
Object refreshTrigger = ReflectionTestUtils.getField(bean,
|
||||
"refreshTrigger");
|
||||
|
||||
assertThat(refreshTrigger).hasFieldOrPropertyWithValue("duration",
|
||||
Duration.ofSeconds(11)).hasFieldOrPropertyWithValue(
|
||||
"validTtlThreshold", Duration.ofSeconds(12));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.vault.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.junit.Test;
|
||||
@@ -26,8 +27,11 @@ import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.vault.authentication.AuthenticationSteps;
|
||||
import org.springframework.vault.authentication.AuthenticationStepsFactory;
|
||||
import org.springframework.vault.authentication.CachingVaultTokenSupplier;
|
||||
import org.springframework.vault.authentication.LifecycleAwareSessionManager;
|
||||
import org.springframework.vault.authentication.ReactiveSessionManager;
|
||||
import org.springframework.vault.authentication.SessionManager;
|
||||
@@ -55,7 +59,7 @@ public class VaultReactiveBootstrapConfigurationTests {
|
||||
public void shouldConfigureTemplate() {
|
||||
|
||||
this.contextRunner.withUserConfiguration(AuthenticationFactoryConfiguration.class)
|
||||
.withPropertyValues("spring.cloud.vault.config.lifecycle.enabled=false")
|
||||
.withPropertyValues("spring.cloud.vault.session.lifecycle.enabled=false")
|
||||
.run(context -> {
|
||||
|
||||
assertThat(context.getBean(ReactiveVaultOperations.class))
|
||||
@@ -86,7 +90,7 @@ public class VaultReactiveBootstrapConfigurationTests {
|
||||
public void shouldConfigureTemplateWithTokenSupplier() {
|
||||
|
||||
this.contextRunner.withUserConfiguration(TokenSupplierConfiguration.class)
|
||||
.withPropertyValues("spring.cloud.vault.config.lifecycle.enabled=false")
|
||||
.withPropertyValues("spring.cloud.vault.session.lifecycle.enabled=false")
|
||||
.run(context -> {
|
||||
|
||||
assertThat(context.getBean(ReactiveVaultOperations.class))
|
||||
@@ -115,6 +119,7 @@ public class VaultReactiveBootstrapConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void sessionManagerBridgeShouldNotCacheTokens() {
|
||||
|
||||
this.contextRunner.withUserConfiguration(TokenSupplierConfiguration.class,
|
||||
CustomSessionManager.class).run(context -> {
|
||||
|
||||
@@ -127,6 +132,55 @@ public class VaultReactiveBootstrapConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDisableSessionManagement() {
|
||||
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.cloud.vault.kv.enabled=false",
|
||||
"spring.cloud.vault.token=foo",
|
||||
"spring.cloud.vault.session.lifecycle.enabled=false")
|
||||
.withBean("vaultTokenSupplier", VaultTokenSupplier.class,
|
||||
() -> Mono::empty)
|
||||
.withBean("taskSchedulerWrapper",
|
||||
VaultBootstrapConfiguration.TaskSchedulerWrapper.class,
|
||||
() -> new VaultBootstrapConfiguration.TaskSchedulerWrapper(
|
||||
new ThreadPoolTaskScheduler()))
|
||||
.run(context -> {
|
||||
|
||||
ReactiveSessionManager bean = context
|
||||
.getBean(ReactiveSessionManager.class);
|
||||
assertThat(bean).isExactlyInstanceOf(CachingVaultTokenSupplier.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConfigureSessionManagement() {
|
||||
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.cloud.vault.kv.enabled=false",
|
||||
"spring.cloud.vault.token=foo",
|
||||
"spring.cloud.vault.session.lifecycle.refresh-before-expiry=11s",
|
||||
"spring.cloud.vault.session.lifecycle.expiry-threshold=12s")
|
||||
.withBean("vaultTokenSupplier", VaultTokenSupplier.class,
|
||||
() -> Mono::empty)
|
||||
.withBean("taskSchedulerWrapper",
|
||||
VaultBootstrapConfiguration.TaskSchedulerWrapper.class,
|
||||
() -> new VaultBootstrapConfiguration.TaskSchedulerWrapper(
|
||||
new ThreadPoolTaskScheduler()))
|
||||
.run(context -> {
|
||||
|
||||
ReactiveSessionManager bean = context
|
||||
.getBean(ReactiveSessionManager.class);
|
||||
|
||||
Object refreshTrigger = ReflectionTestUtils.getField(bean,
|
||||
"refreshTrigger");
|
||||
|
||||
assertThat(refreshTrigger).hasFieldOrPropertyWithValue("duration",
|
||||
Duration.ofSeconds(11)).hasFieldOrPropertyWithValue(
|
||||
"validTtlThreshold", Duration.ofSeconds(12));
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class AuthenticationFactoryConfiguration {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user