visit
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
LeaseListenener
via addLeaseListener()
whichrequestRotatingSecret()
on the SecretLeaseContainer
when the dynamic database lease expiresSecretLeaseCreatedEvent
with mode ROTATE
) by:HikariConfigMXBean
of the HikariDataSource
HikariPoolMXBean
to use the new credentialsplugins {
id("org.springframework.boot") version "2.2.4.RELEASE" // <1>
id("io.spring.dependency-management") version "1.0.9.RELEASE" // <2>
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa") // <3>
runtimeOnly("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
spring-boot-starter-data-jpa
dependency together with Spring Boot 2.x you automatically get HikariCP(Rotation at runtime - Image by from )
To rotate the database credentials, which are dynamic secret from Hashicorp Vaults point of view, we have to do following steps:To detect when the database credentials are expiring we can use the same approach like we did to . Let’s again autowire the
SecretLeaseContainer
and the database role which is configured as the property spring.cloud.vault.database.role
to the VaultConfig
configuration class:@Configuration
class VaultConfig(
private val leaseContainer: SecretLeaseContainer,
@Value("\${spring.cloud.vault.database.role}")
private val databaseRole: String
) {
As before in a
@PostConstruct
method you can then add the additional LeaseListenener
which does the lease rotation:@PostConstruct
private fun postConstruct() {
val vaultCredsPath = "database/creds/$databaseRole"
leaseContainer.addLeaseListener { event ->
if (event.path == vaultCredsPath) {
log.info { "Lease change for DB: ($event) : (${event.lease})" }
if (event.isLeaseExpired && event.mode == RENEW) {
// TODO Rotate the credentials here <1>
}
}
}
}
(The credentials should be renewed - Image by from )
When the lease for the database credentials expire we have to request a new secret.if (event.isLeaseExpired && event.mode == RENEW) {
log.info { "Replace RENEW for expired credential with ROTATE" }
leaseContainer.requestRotatingSecret(vaultCredsPath) // <1>
}
The returned value of
requestRotatingSecret()
is of type :Represents a requested secret from a specific Vault path associated with a lease .A can be renewing or rotating.— Spring Vault Javadoc
As mentioned in the Javadoc, the
RequestedSecret
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?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.if (event.isLeaseExpired && event.mode == RENEW) {
log.info { "Replace RENEW for expired credential with ROTATE" }
leaseContainer.requestRotatingSecret(vaultCredsPath) // <1>
} else if (event is SecretLeaseCreatedEvent && event.mode == ROTATE) { // <2>
val credentials = event.credentials // <3>
// TODO Update database connection
}
<1> The rotating secret is requested
<2> The new secret event is a rotating
SecretLeaseCreatedEvent
The contains the new credentials requested from Hashicorp Vault. The
event.credentials
is an (see code below).Details of extracting the secrets safely
The
SecretLeaseCreatedEvent
contains a Map<String, Object>
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 ConfigurableApplicationContext
to shut down the Spring application. Let’s add this as another autowired dependency to this class:@Configuration
class VaultConfig(
private val leaseContainer: SecretLeaseContainer,
@Value("\${spring.cloud.vault.database.role}")
private val databaseRole: String
) {
Now we can extract the credentials from the event. The extension property
event.credentials
returns null
if the credentials cannot be received. With the ConfigurableApplicationContext
we can handle this error case:if (credentials == null) {
log.error { "Cannot get updated DB credentials. Shutting down." }
applicationContext.close() // <1>
return@addLeaseListener // <2>
}
refreshDatabaseConnection(credentials) // <3>
<1> If we cannot get the renewed credentials shutdown the application
<2> because of the return from the lambda,
credentials
is smart casted to a non-nullable value after the if
block. Kotlin is awesome!credentials
cannot be null
and can be used to Now let’s see how the credentials are retrieved from the event within the extension property:private val SecretLeaseCreatedEvent.credentials: Credential?
get() {
val username = get("username") ?: return null // <1>
val password = get("password") ?: return null // <1>
return Credential(username, password)
}
private fun SecretLeaseCreatedEvent.get(param: String): String? {
return secrets[param] as? String // <2>
}
private data class Credential(val username: String, val password: String)
<1> username and password are extracted using the extension method
get()
. If one of the get()
calls return null
then null
is returned instead of a Credential
String
. If the entry does not exist in the map or is not a String
then null
is returned(Refreshed version of access restriction - Image by from )
Now that we know the new credentials we have to ensure that these fresh secrets are used instead of the old ones.private fun refreshDatabaseConnection(credential: Credential) {
updateDbProperties(credential) // <1>
updateDataSource(credential) // <2>
}
<1> first update the database system properties
<2> finally update the datasource to use the newly created credentials
@Configuration
class VaultConfig(
private val applicationContext: ConfigurableApplicationContext,
private val hikariDataSource: HikariDataSource,
private val leaseContainer: SecretLeaseContainer,
@Value("\${spring.cloud.vault.database.role}")
private val databaseRole: String
) {
Utilizing the
HikariDataSource
we can update the database credentials used by the Spring application:private fun updateDbProperties(credential: Credential) { // <1>
val (username, password) = credential
System.setProperty("spring.datasource.username", username)
System.setProperty("spring.datasource.password", password)
}
private fun updateDataSource(credential: Credential) {
val (username, password) = credential
log.info { "==> Update database credentials" }
hikariDataSource.hikariConfigMXBean.apply { // <2>
setUsername(username)
setPassword(password)
}
hikariDataSource.hikariPoolMXBean?.softEvictConnections() // <3>
?.also { log.info { "Soft Evict Hikari Data Source Connections" } }
?: log.warn { "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
HikariDataSource
we can get the which allows setting the new credentialsLease 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
Originally published at on February 18, 2020