Custom client-side password decryption and Spring Cloud Config Server

When using Spring Cloud Config Server it usually doesn’t take long before passwords end up in the underlying GIT repository as well. Using the {cipher} prefix and symmetric or private/public key (stored in a keystore), it is possible to store encrypted versions of these passwords, such that they don’t appear in plaintext in the GIT repository. By default, the config server decrypts them before sending them to the client application.

This blog posts follows a slightly different use case and discusses some modification to the default approach:

  • Decrypting at the client side
  • Decryption using a custom algorithm
  • Using autoconfiguration to do this transparently

The example I’ll construct has several elements: a configuration server, a client application and a library that will do the decryption transparently.

This blog post was written for Spring Cloud Config 1.2.2.RELEASE. The code can be found at https://github.com/kuipercm/custom-spring-cloud-config-decryption-example.

Situation

The requirements we’re trying to satisfy are as follows:

  1. It is not allowed to send plaintext passwords ‘over the line’. This is a security requirement, even when using HTTPS connections with mutual authentication and a cipher.
  2. The decryption should be transparent to the application: it shouldn’t know that the passwords are encrypted, nor what kind of algorithm they use
  3. There is a custom, business-approved encryption/decryption methodology in place. For the purposes of this blog post, the algorithm is simply to reverse the encrypted password, since it is an easy method to demonstrate. In reality you can think of any other method. The method used in this example is not secure at all, so don’t use it in real applications!

Configuration Server

To get started, let’s first create a “default” configuration server.

@SpringBootApplication
@EnableConfigServer
public class ConfigurationServer {
    public static void main(String[] args) {
        SpringApplication.run(ConfigurationServer.class, args);
    }
}

By setting the appropriate properties, the configuration server will know which GIT repository to clone and to expose through its endpoints. This is all default behavior and is well documented in the documentation.

It is however important to note that the property to send decrypted passwords to clients has been disabled: spring.cloud.config.server.encrypt.enabled=false. This way the clients are in charge of decrypting the encrypted data.

Configuration Client

The client is also relatively straightforward.

@SpringBootApplication
public class ConfigurationClient {
    public static void main(String[] args) {
        SpringApplication.run(ConfigurationClient.class, args);
    }
}

where this application has a dependency on Spring Cloud Config Client in its pom.xml. (For full details, see the git repository.)

The client application also contains a repository class which connects to the in-memory h2 database using HikariCP. The properties for this connection, including the encrypted password, are in the configuration repository.

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: johnsmith
    password: '{cipher}drowssapxelpmocym'
    hikari:
      idle-timeout: 10000

Since we told the server application not to decrypt encrypted properties before sending them to the client, the client application will receive the properties as described above. So, now it’s up to the client to decrypt these properties.

The decryption library

There are several options to facilitate the decryption.

The most naive implementation I can come up with is to create a Utility class that is called manually to decrypt the properties as needed. This works, but requires us to inject properties manually, decrypt them and then use them to construct the objects we want. In the case of the Hikari datasource, this approach makes autoconfiguration of the object impossible and we’d have to do it by hand. Probably we can do better.

An alternative, less naive, approach is to use an application event, decrypt all encrypted properties upon this event and place them back in the Spring environment. That way we would still be able to use the default autoconfiguration behavior and the whole decryption part is transparent to the rest of the application, which is nice. The downside is that we have to have a lot of knowledge about the internals of the configuration client. For example, we have to know how the properties are inserted into the Spring context by the client such that we can do the right things when trying to decrypt them. Should the internals of the configuration client change, there is a good chance we have to change our code as well.

The cleanest solution I’ve found is to tap into the mechanism used by the configuration client when decrypting the encrypted values and substitute an alternative decryption strategy to be used.

The TextExcryptor

At the basis of the whole encryption/decryption mechanism is the so-called TextEncryptor and, despite what its name suggest, it is used to both encrypt and decrypt. In this example, the TextEncryptor is very, very simple, so once again the warning: don’t try this at home! This implementation is not secure at all, but is for demonstration purposes only.

public class ReversableEncryptor implements TextEncryptor {
    @Override
    public String encrypt(String toEncrypt) {
        return new StringBuilder().append(toEncrypt).reverse().toString();
    }

    @Override
    public String decrypt(String toDecrypt) {
        return new StringBuilder().append(toDecrypt).reverse().toString();
    }
}

Please note that this implementation doesn’t do anything with the prefix ‘{cipher}’. This will already be stripped by the configuration client.

Tap into autoconfiguration

Now we need to make sure that the custom encryptor is picked up by the Spring configuration and that it replaces other encryptors. The easiest way to achieve this is by using Spring Boots autoconfiguration to load the encryptor.

@Configuration
public class CustomEncryptorBootstrapConfiguration {
    public static final String CUSTOM_ENCRYPT_PROPERTY_NAME = "bldn.encryption";
    public static final String REVERSABLE_ENCRYPTION = "reversable";

    @Configuration
    @ConditionalOnProperty(name = CUSTOM_ENCRYPT_PROPERTY_NAME, havingValue = REVERSABLE_ENCRYPTION)
    protected static class ReverableEncryptorConfiguration {
        @Bean
        @ConditionalOnMissingBean(ReversableEncryptor.class)
        public TextEncryptor reversableEncryptor() {
            return new ReversableEncryptor();
        }
    }
}

In this configuration, our ReversableEncryptor is loaded when there is a property “bldn.encryption” present with the value “reverable”. This property can be provided via de commandline or via other means, although it is not advisable to provide it through the configuration server (as it might come too late then).

To actually load this Configuration as part of the autoconfiguration of the application, we need to add a spring.factories file with (at least) the following content

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
nl.bldn.projects.customdecryption.CustomEncryptorBootstrapConfiguration

This will make sure the CustomEncryptorBootstrapConfiguration is loaded during application startup and when the require property is present, the custom TextEncryptor will take the place of the default one.

When the properties are now received by the client, the custom decryption mechanism will kick in and the application will use the decrypted properties for other autoconfigured beans.

Final Remarks

This blog post has shown a way to setup custom decryption of encrypted properties received from a Spring Cloud Config Server. In this scenario the decryption is left to the client application and the method of decryption cannot be captured by the default methods available in Spring Cloud Config.

The code of this example can be found at https://github.com/kuipercm/custom-spring-cloud-config-decryption-example. To test out the code, follow the instructions of the readme file.

Happy coding!