Rotate Expiring Spring Cloud Vault Database Credentials Without Downtime TL;DR The first episode of this series of blog posts can be found here: Hashicorp Vault max_ttl Killed My Spring App It is possible to rotate the Spring Cloud Vault database credentials at runtime for relational databases if you use HikariCP. To do so add a via which LeaseListenener addLeaseListener() calls on the when the dynamic database lease expires requestRotatingSecret() SecretLeaseContainer reacts on a with mode ) by: SecretLeaseCreatedEvent ROTATE setting username and password on the of the HikariConfigMXBean HikariDataSource call softEvictConnections() on the to use the new credentials HikariPoolMXBean New Spring Cloud Vault database secrets without downtime This is the second episode in a series of blog post about how to handle the expiration of Hashicorp Vault generated dynamic database credentials in a Spring application. Spring leaves your application without a database connection when these credentials expire. For more context and some general solutions please check the . first post This time I would like to show you how to renew the database credentials at runtime. So, this time you neither need to nor to nor do you have to , which could potentially result in downtime or not met . regularly restart or redeploy your application use a (probably too) long maximum time-to-live for the credentials programmatically restart the application SLAs As we all know . The costs for the approach I am presenting you this time are: there ain’t no such thing as a free lunch more implementation effort stricter prerequisites (only relational databases supported) The first bullet point is addressed because this post should help you with the implementation. This leaves us with the… Prerequisites This approach is only applicable for Spring applications which use . HikariCP Luckily the usual way to store and retrieve data in a relational database with is to use . In Spring Boot 2, Hikari is the default DataSource implementation, which makes it typical setup for using relational databases. Spring Boot Spring Data JPA Show me the code To fulfill the prerequisites, it is enough to depend on the Spring Boot JPA Starter: plugins { id( ) version id( ) version } dependencies { implementation( ) runtimeOnly( ) } "org.springframework.boot" "2.2.4.RELEASE" // <1> "io.spring.dependency-management" "1.0.9.RELEASE" // <2> "org.springframework.boot:spring-boot-starter-data-jpa" // <3> "org.postgresql:postgresql" // <4> <1> You don’t have to use the Spring Boot Gradle plugin, but it makes your live easier <2> The Spring dependency-management plugin together with Spring Boot Gradle plugin ensures that all Spring related dependencies have the version being compatible with the Spring Boot version <3> By adding the dependency together with Spring Boot 2.x you automatically get HikariCP <4> In my example I use PostgreSQL but also most other relational databases, like MySQL, would work spring-boot-starter-data-jpa These few lines are basically enough to meet the requirements of using HikariCP, in this case with PostgreSQL. Rotating the expiring database credentials at runtime (Rotation at runtime - Image by Peter H from pixabay ) To rotate the database credentials, which are dynamic secret from Hashicorp Vaults point of view, we have to do following steps: Detect when the database credentials are expiring Get new dynamic database credentials from Hashicorp Vault Refresh the database connections to use the new credentials Detect when the database credentials are expiring To detect when the database credentials are expiring we can use the same approach like we did to . Let’s again autowire the and the database role which is configured as the property to the configuration class: restart the application when credentials expire in the first blog post SecretLeaseContainer spring.cloud.vault.database.role VaultConfig ( leaseContainer: SecretLeaseContainer, databaseRole: String ) { @Configuration class VaultConfig private val @Value( ) "\${spring.cloud.vault.database.role}" private val As before in a method you can then add the additional which does the lease rotation: @PostConstruct LeaseListenener { vaultCredsPath = leaseContainer.addLeaseListener { event -> (event.path == vaultCredsPath) { log.info { } (event.isLeaseExpired && event.mode == RENEW) { } } } } @PostConstruct private fun postConstruct () val "database/creds/ " $databaseRole if "Lease change for DB: ( ) : ( )" $event ${event.lease} if // TODO Rotate the credentials here <1> <1> When this code path is reached, the database secret expired Next step is to… Get new dynamic database credentials from Hashicorp Vault (The credentials should be renewed - Image by pasja1000 from pixabay ) When the lease for the database credentials expire we have to request a new secret. (event.isLeaseExpired && event.mode == RENEW) { log.info { } leaseContainer.requestRotatingSecret(vaultCredsPath) } if "Replace RENEW for expired credential with ROTATE" // <1> <1> Tells Spring Vault to request a new rotating database secret The returned value of is of type : requestRotatingSecret() RequestedSecret Represents a requested secret from a specific Vault path associated with a lease . RequestedSecret.Mode A can be renewing or rotating. RequestedSecret — Spring Vault Javadoc As mentioned in the Javadoc, the contains the path and the mode of the secret, but it does not contain the secret itself. So how do we get the requested credentials? RequestedSecret We have just requested a new rotating database secret within our own . This listener receives s which are also created, when a new rotating secret is received. This is exactly what we need! So, let’s also react on this kind of event. LeaseListener SecretLeaseEvent (event.isLeaseExpired && event.mode == RENEW) { log.info { } leaseContainer.requestRotatingSecret(vaultCredsPath) } (event SecretLeaseCreatedEvent && event.mode == ROTATE) { credentials = event.credentials } if "Replace RENEW for expired credential with ROTATE" // <1> else if is // <2> val // <3> // TODO Update database connection <1> The rotating secret is requested <2> The new secret event is a rotating <3> The event contains the new database credentials SecretLeaseCreatedEvent The contains the new credentials requested from Hashicorp Vault. The is an (see code below). SecretLeaseCreatedEvent event.credentials property extension property Details of extracting the secrets safely The contains a with the secrets, so there is no typesafe option to get the database credentials. If for some reason the event does not contain the credentials we are again in the situation, that we cannot contact the database anymore. In that case I would prefer to shut down the application. That’s why we need the to shut down the Spring application. SecretLeaseCreatedEvent Map<String, Object> ConfigurableApplicationContext Let’s add this as another autowired dependency to this class: ( leaseContainer: SecretLeaseContainer, databaseRole: String ) { @Configuration class VaultConfig private val @Value( ) "\${spring.cloud.vault.database.role}" private val Now we can extract the credentials from the event. The extension property returns if the credentials cannot be received. With the we can handle this error case: event.credentials null ConfigurableApplicationContext (credentials == ) { log.error { } applicationContext.close() } refreshDatabaseConnection(credentials) if null "Cannot get updated DB credentials. Shutting down." // <1> return @addLeaseListener // <2> // <3> <1> If we cannot get the renewed credentials shutdown the application <2> because of the return from the lambda, is smart casted to a non-nullable value after the block. Kotlin is awesome! <3> here the cannot be and can be used to credentials if credentials null refresh the database connection Now let’s see how the credentials are retrieved from the event within the extension property: SecretLeaseCreatedEvent.credentials: Credential? () { username = ( ) ?: password = ( ) ?: Credential(username, password) } : String? { secrets[param] ? String } ( username: String, password: String) private val get val get "username" return null // <1> val get "password" return null // <1> return private SecretLeaseCreatedEvent. fun get (param: ) String return as // <2> private data class Credential val val <1> username and password are extracted using the extension method . If one of the calls return then is returned instead of a <2> the secret is read out of the map and with to a . If the entry does not exist in the map or is not a then is returned get() get() null null Credential safe casted as? String String null Refresh the database connection (Refreshed version of access restriction - Image by Nenad Maric from pixabay ) Now that we know the new credentials we have to ensure that these fresh secrets are used instead of the old ones. { updateDbProperties(credential) updateDataSource(credential) } private fun refreshDatabaseConnection (credential: ) Credential // <1> // <2> <1> first update the database system properties <2> finally update the datasource to use the newly created credentials To update the datasource credentials we need the . So, let’s add this also to the constructor: HikariDataSource ( applicationContext: ConfigurableApplicationContext, hikariDataSource: HikariDataSource, leaseContainer: SecretLeaseContainer, databaseRole: String ) { @Configuration class VaultConfig private val private val private val @Value( ) "\${spring.cloud.vault.database.role}" private val Utilizing the we can update the database credentials used by the Spring application: HikariDataSource { (username, password) = credential System.setProperty( , username) System.setProperty( , password) } { (username, password) = credential log.info { } hikariDataSource.hikariConfigMXBean.apply { setUsername(username) setPassword(password) } hikariDataSource.hikariPoolMXBean?.softEvictConnections() ?.also { log.info { } } ?: log.warn { } } private fun updateDbProperties (credential: ) Credential // <1> val "spring.datasource.username" "spring.datasource.password" private fun updateDataSource (credential: ) Credential val "==> Update database credentials" // <2> // <3> "Soft Evict Hikari Data Source Connections" "CANNOT Soft Evict Hikari Data Source Connections" <1> Updating the database system properties is technically not mandatory but ensures consistency, if other parts of the system rely these properties being accurate <2> From the we can get the which allows setting the new credentials <3> As the final step, all connections have to be evicted to use the new credentials HikariDataSource HikariConfigMXBean Summary With these steps the PostgreSQL or other relational database credentials can be rotated, when there Hashicorp Vault leases expire. This works at runtime and without downtime. The logs will look something like this: Lease change for DB: (org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent[source=RequestedSecret [path='database/creds/readonly', mode=RENEW]]) : (Lease [leaseId='database/creds/readonly/wzUQ81Ng4YQcBwdAyLrSZSvd', leaseDuration=PT10S, renewable=true]) Replace RENEW for expired credential with ROTATE Lease change for DB: (org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent[source=RequestedSecret [path='database/creds/readonly', mode=ROTATE]]) : (Lease [leaseId='database/creds/readonly/ur8C5V1wJMSAdiatwkWXCi03', leaseDuration=PT30S, renewable=true]) ==> Update database credentials Soft Evict Hikari Data Source Connections The complete repository can be found . on GitHub Finally the handling of expiring Hashicorp Vault database secrets in a Spring application is production-ready. Originally published at https://secrets-as-a-service.com on February 18, 2020 Image Source