Bootstrapping Jasypt
Making Jasypt cloud-ready
Recently, one of my juniors was facing an issue with encrypted passwords in Spring config server. This post is to discuss that issue and how the issue was resolved.
Please read on https://blog.thesoftwarefeed.com/p/bootstrapping-jasypt for the latest of this article.
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 within the microservice (i.e. config clients) and is having the 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 the 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 a 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. Check out 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
@ConditionalOnClass("BootstrapApplicationListener")
is to make sure that this configuration will be effective in spring cloud based environments only. However since the 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.