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:
Mark Paluch
2020-08-11 17:07:37 +02:00
parent 254342bb73
commit 2e4bf3d0cd
8 changed files with 244 additions and 15 deletions

View File

@@ -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]

View File

@@ -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);

View File

@@ -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()) {

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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");
}

View File

@@ -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));
});
}
}

View File

@@ -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 {