Resolving Encrypted Values in @ConfigurationProperties Injection Despite Environment Variable Decryption
In a microservice application, a configuration property test.container-name is defined in application.properties as:
test.container-name=Tomcat
A Java class TestConfigProperty uses @ConfigurationProperties to inject this property:
@ConfigurationProperties(prefix = "test")
@Component
public class TestConfigProperty {
private String containerName;
public String getContainerName() {
return containerName;
}
public void setContainerName(String containerName) {
this.containerName = containerName;
}
}
To secure sensitive data, test.container-name is encrypted and referenced via an environment varible:
test.container-name=${TEST_CONTAINER_NAME}
The environment variable holds an encrypted value (e.g., Base64-encoded). A custom DecryptEnvironmentPostProcessor extends EnvironmentPostProcessor to decrypt values prefixed with ENC_ at startup:
public class DecryptEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
private static final String DECRYPTED_SOURCE_NAME = "decryptedSystemEnvironment";
@Override
public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication app) {
String sysEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
MapPropertySource sysEnvSource = (MapPropertySource) env.getPropertySources().get(sysEnvName);
Map<String, Object> decryptedValues = new HashMap<>();
if (sysEnvSource == null) {
return;
}
sysEnvSource.getSource().forEach((key, value) -> {
if (value instanceof String strVal) {
if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) {
String decrypted = new String(Base64.getDecoder().decode(strVal.substring(4)));
decryptedValues.put(key, decrypted);
}
}
});
if (!decryptedValues.isEmpty()) {
MapPropertySource decryptedSource = new MapPropertySource(DECRYPTED_SOURCE_NAME, decryptedValues);
env.getPropertySources().addBefore(sysEnvName, decryptedSource);
}
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
Despite decryption, TestConfigProperty.containerName receives the encrypted value. Debugging shows the decrypted PropertySource precedes the encrypted one, yet injection yields the encrypted value.
Root Cause Analysis
Property binding for @ConfigurationProperties occurs in ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(), which delegates to ConfigurationPropertiesBinder.bind(). This calls Binder.bind(), passing the prefix test to create a ConfigurationPropertyName. The process recursively binds properties via bindObject() and bindDataObject().
For JavaBeans, JavaBeanBinder.bind() retrieves all BeanProperty instances. The property containerName is normalized to dash format (container-name). The binder appends this to the prefix, forming test.container-name, and recursively searches for this property in ConfigurationPropertySource instances.
SpringIterableConfigurationPropertySource adapts PropertySource objects. The decrypted PropertySource (MapPropertySource) is placed before the encrypted SystemEnvironmentPropertySource. However, SystemEnvironmentPropertySource uses SystemEnvironmentPropertyMapper, which maps test.container-name to TEST_CONTAINERNAME and TEST_CONTAINER_NAME. The encrypted source contains TEST_CONTAINER_NAME=ENC_VG9tY2F0, while the decrypted MapPropertySource only has TEST_CONTAINER_NAME=Tomcat and lacks the SystemEnvironmentPropertyMapper. Thus, when searching for test.container-name:
- The decrypted
MapPropertySource(withDefaultPropertyMapper) mapstest.container-nametotest.container-name, which doesn't matchTEST_CONTAINER_NAME. - The encrypted
SystemEnvironmentPropertySource(withSystemEnvironmentPropertyMapper) mapstest.container-nametoTEST_CONTAINER_NAME, retrieving the encrypted valueENC_VG9tY2F0.
Solution
Modify DecryptEnvironmentPostProcessor to use SystemEnvironmentPropertySource for the decrypted properties, ensuring it includes SystemEnvironmentPropertyMapper:
@Override
public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication app) {
String sysEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
MapPropertySource sysEnvSource = (MapPropertySource) env.getPropertySources().get(sysEnvName);
Map<String, Object> decryptedValues = new HashMap<>();
if (sysEnvSource == null) {
return;
}
sysEnvSource.getSource().forEach((key, value) -> {
if (value instanceof String strVal) {
if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) {
String decrypted = new String(Base64.getDecoder().decode(strVal.substring(4)));
decryptedValues.put(key, decrypted);
}
}
});
if (!decryptedValues.isEmpty()) {
// Use SystemEnvironmentPropertySource instead of MapPropertySource
env.getPropertySources().addBefore(sysEnvName,
new SystemEnvironmentPropertySource(DECRYPTED_SOURCE_NAME, decryptedValues));
}
}
This ensures the decrypted source uses the same property mapping as the original system environment, allowing test.container-name to correctly resolve to the decrypted value.