Recently, one of my juniors was facing an issue with encrypted passwords in Spring config server. This post is to discuss about that issue and how the issue was resolved.
Background
Spring cloud defines a Bootstrap phase wherein it creates a context from configurations in the boostrap.properties
(or .yml) file. The boostrap.properties
file is usually packaged in microservices (i.e. config clients) and having following 2 configurations.
spring.application.name=microservice-x
spring.cloud.config.uri=http://host-y:8888/
During the bootstrap phase the microservice-x
will retrieve properties from the config server running at the endpoint http://host-y:8888/
and then constructs the beans.
The config server however doesn’t have a bootstrap phase (typical case) and having following properties in its application.properties
.
spring.application.name=config-server
spring.cloud.config.server.git.uri=https://github.com/fahimfarookme/config-repo
But what if the config repository is a protected one? The credentials should also be configured.
spring.cloud.config.server.git.username=myusername@somewhere.com
spring.cloud.config.server.git.password=mypassword
However it’s a security vulnerability to keep passwords in plain text. Now that Jasypt comes to rescue. Let’s encrypt the password with Jasypt and the final application.properties
would look like this;
spring.application.name=config-server
spring.cloud.config.server.git.uri=https://github.com/fahimfarookme/config-repo
spring.cloud.config.server.git.username=myusername@somewhere.com
spring.cloud.config.server.git.password=ENC(VMAckVbiEpU1pGpcZoow=)
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.password=passphrase
We’ve configured the encryption algorithm and the symmetric key as well above. Also we might have to secure the jasypt.encryptor.password
which is out of the scope for this post.
So far, so good!
Issue
Sometimes it’s required to bootstrap the config-server itself. i.e. the config server should configure its beans from the properties fetched from the remote repository. In our case we extended the config server with a dashboard - which was dependent on some environment specific configurations from the remote repository.
Let’s bootstrap the config server by introducing a bootstrap.properties
file. The configurations in this file are similar to application.properties
file above, except the instruction to bootstrap.
# bootstrap the config server.
spring.cloud.config.server.bootstrap=true
# same as application.properties before.
spring.application.name=config-server
spring.cloud.config.server.git.uri=https://github.com/fahimfarookme/config-repo
spring.cloud.config.server.git.username=myusername@somewhere.com
spring.cloud.config.server.git.password=ENC(VMAckVbiEpU1pGpcZoow=)
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.password=passphrase
This will introduce a bootstrap phase for config server during which it will try to pull configurations from the remote repository. However the password for config repository is encrypted with Jasypt and Jasypt decrypts properties only at a later stage (post bootstrap phase).
I could see the following exception in the logs.
Caused by: org.eclipse.jgit.errors.TransportException: https://github.com/fahimfarookme/config-repo: not authorized
Analysis
The bootstrap context is created from the sources defined under BootstrapConfiguration
in the spring.factories
. The spring-cloud-context
defines following BootstrapConfiguration
in it’s spring.factories
.
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration
The PropertySourceBootstrapConfiguration
is an ApplicationContextInitializer
which tries to pull configurations from the remote config repository during initialization. It also uses the credentials provided in order to establish the connection with the remote repository. Also it’s having the order of Ordered.HIGHEST_PRECEDENCE + 10
.
Jasypt on the other hand defines an auto-configuration in it’s spring.factories
.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ulisesbocchio.jasyptspringboot.JasyptSpringBootAutoConfiguration
This configuration class defines EnableEncryptablePropertiesBeanFactoryPostProcessor
which is a BeanFactoryPostProcessor
and is responsible for decrypting encrypted text in the property files. Also it’s having the order of Ordered.LOWEST_PRECEDENCE
.
However this BeanFactoryPostProcessor
is initialized only after the bootstrap phase - hence, by the time the config server (i.e. PropertySourceBootstrapConfiguration
) is trying to pull the configurations from the remote config repository, it doesn’t have the decrypted password.
Solution
As the solution, we need Jasypt to decrypt encrypted properties before config-server starts pulling configurations from config repository.
How about changing the order of EnableEncryptablePropertiesBeanFactoryPostProcessor
to Ordered.HIGHEST_PRECEDENCE + 11
? Well, that won’t work because the child context JasyptSpringBootAutoConfiguration
itself is constructed at a later stage.
The solution I proposed was to bootstrap Jasypt in a spring cloud based environment. Checkout this pull request. The solution in brief is to define a new BootstrapConfiguration
in spring.factories
of Jasypt which is similar to JasyptSpringBootAutoConfiguration
so that EnableEncryptablePropertiesBeanFactoryPostProcessor
is initialized during the bootstrap phase. However it’s not required to redefine this BeanFactoryPostProcessor
with a higher priority because post-processing of bean factories happen prior to initializing Initializers. i.e. EnableEncryptablePropertiesBeanFactoryPostProcessor#postProcessBeanFactory()
is invoked before PropertySourceBootstrapConfiguration#initialize()
anyways.
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.ulisesbocchio.jasyptspringboot.JasyptSpringCloudBootstrapConfiguration
@Configuration
@ConditionalOnClass(name = "org.springframework.cloud.bootstrap.BootstrapApplicationListener")
@ConditionalOnProperty(name = "spring.cloud.bootstrap.enabled", havingValue = "true", matchIfMissing = true)
public class JasyptSpringCloudBootstrapConfiguration {
@Configuration
@ConditionalOnProperty(name = "jasypt.encryptor.bootstrap", havingValue = "true", matchIfMissing = true)
@Import(EnableEncryptablePropertiesConfiguration.class)
protected static class BootstrappingEncryptablePropertiesConfiguration {
}
}
@ConditionalOnClass("BootstrapApplicationListener")
is to make sure that this configuration will be effective in spring cloud based environments only. However since bootstrap phase can be turned off by spring.cloud.bootstrap.enabled=false
configuration, @ConditionalOnProperty("spring.cloud.bootstrap.enabled" ...)
ensures that it’s not the case. I also provided an additional jasypt.encryptor.bootstrap
property in order to allow disabling the Jasypt-bootstrapping process altogether, in which case Jasypt will be auto-configured as usual.