diff --git a/docs/src/main/asciidoc/spring-cloud-vault.adoc b/docs/src/main/asciidoc/spring-cloud-vault.adoc index 26773ccf..2801245d 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault.adoc @@ -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] diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java index 6d88f575..8819e8a0 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java @@ -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 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); diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java index 913735d2..dd83d431 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java @@ -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()) { diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java index 16e04da8..980edd33 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java @@ -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; + } + + } + } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java index 22aec5c3..ff6b1020 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java @@ -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); diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java index 64c99c89..3798261d 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java @@ -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"); } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapConfigurationTests.java index 9279e668..2fa50f00 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapConfigurationTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapConfigurationTests.java @@ -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)); + }); + } + } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java index 48c8bfcd..a6aa5f08 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java @@ -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 {