Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/modules/ROOT/pages/server/environment-repository.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,52 @@ You can set `spring.cloud.config.server.accept-empty` to `false` so that Server

NOTE: You cannot place `spring.main.*` properties in a remote `EnvironmentRepository`. These properties are used as part of the application initialization.

== File Content Resolution

The Config Server can resolve the content of local files and serve them as Base64 encoded values.
This is particularly useful for serving binary files, such as SSL keystores or certificates, within configuration properties.

To enable this feature, you must set the following property (it is disabled by default for security reasons):

[source,yaml]
----
spring:
cloud:
config:
server:
file-resolving:
enabled: true
----

WARNING: Enabling this feature allows the Config Server to read files from the local file system where the server is running. Ensure that the process has appropriate file system permissions and is running in a secure environment.

==== Usage

You can use the `{file}` prefix in your configuration values followed by the path to the file.
The Config Server will read the file, encode its content to a Base64 string, and replace the property value.

**1. Absolute Path**

You can reference files on the Config Server's local file system using an absolute path:

[source,yaml]
----
server:
ssl:
key-store: {file}/etc/certs/keystore.jks
----

**2. Relative Path (Repository-aware)**

If you are using a repository that supports search paths (like Git, SVN, or Native), you can reference files **relative to the repository root** by starting the path with a dot (`.`):

[source,yaml]
----
server:
ssl:
# Resolves 'certs/keystore.jks' located inside the Git repository
key-store: {file}./certs/keystore.jks
----

In this case, the Config Server will look for the file inside the cloned repository directory.
If the repository does not support search paths (e.g., JDBC, Vault), relative paths will be ignored.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.config.server.config;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.cloud.config.server.environment.FileResolvingEnvironmentRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
* Autoconfiguration for {@link FileResolvingEnvironmentRepository}.
* Wraps the existing EnvironmentRepository to support file content resolution.
*
* @author Johny Cho
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(EnvironmentRepositoryConfiguration.class)
public class FileResolvingEnvironmentRepositoryConfiguration {

@Bean
@Primary
@ConditionalOnBean(EnvironmentRepository.class)
@ConditionalOnProperty(value = "spring.cloud.config.server.file-resolving.enabled", havingValue = "true")
public FileResolvingEnvironmentRepository fileResolvingEnvironmentRepository(EnvironmentRepository environmentRepository) {
return new FileResolvingEnvironmentRepository(environmentRepository);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.config.server.environment;

import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.FileCopyUtils;

/**
* @author Johny Cho
*/
public class FileResolvingEnvironmentRepository implements EnvironmentRepository, SearchPathLocator {

private static final Log log = LogFactory.getLog(FileResolvingEnvironmentRepository.class);

private final EnvironmentRepository delegate;

private final ResourceLoader resourceLoader = new DefaultResourceLoader();

private static final String PREFIX = "{file}";

public FileResolvingEnvironmentRepository(EnvironmentRepository delegate) {
this.delegate = delegate;
}

@Override
public Environment findOne(String application, String profile, String label) {
Environment env = this.delegate.findOne(application, profile, label);
if (Objects.isNull(env)) {
return null;
}

Locations locations = resolveLocations(application, profile, label);
List<PropertySource> sources = env.getPropertySources();

for (int i = 0; i < sources.size(); i++) {
PropertySource source = sources.get(i);
PropertySource resolvedSource = processPropertySource(source, locations);
if (Objects.nonNull(resolvedSource)) {
sources.set(i, resolvedSource);
}
}

return env;
}

@Override
public Locations getLocations(String application, String profile, String label) {
return resolveLocations(application, profile, label);
}

private Locations resolveLocations(String application, String profile, String label) {
if (this.delegate instanceof SearchPathLocator locator) {
return locator.getLocations(application, profile, label);
}
return new Locations(application, profile, label, null, new String[0]);
}

/**
* Process a single PropertySource. Returns a new PropertySource if modification occurred, otherwise null.
*/
private PropertySource processPropertySource(PropertySource source, Locations locations) {
Map<?, ?> originalMap = source.getSource();
Map<Object, Object> modifiedMap = new LinkedHashMap<>(originalMap);
boolean modified = false;

for (Map.Entry<?, ?> entry : originalMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof String str && str.startsWith(PREFIX)) {
String path = str.substring(PREFIX.length());
String resolvedValue = resolveFileContent(entry.getKey().toString(), path, locations);
if (Objects.nonNull(resolvedValue)) {
modifiedMap.put(entry.getKey(), resolvedValue);
modified = true;
}
}
}

return modified ? new PropertySource(source.getName(), modifiedMap) : null;
}

private String resolveFileContent(String key, String path, Locations locations) {
try {
Resource resource = findResource(path, locations);
if (Objects.nonNull(resource) && resource.isReadable()) {
byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
return Base64.getEncoder().encodeToString(content);
}
}
catch (IOException e) {
log.warn(String.format("Failed to resolve file content for '%s'. path: %s", key, path), e);
}
return null;
}

private Resource findResource(String path, Locations locations) {
// 1. Try relative path if locations are available
if (path.startsWith(".") && Objects.nonNull(locations) && Objects.nonNull(locations.getLocations())) {
for (String location : locations.getLocations()) {
String resourceLocation = location + (location.endsWith("/") ? "" : "/") + path;
Resource candidate = this.resourceLoader.getResource(resourceLocation);
if (candidate.exists() && candidate.isReadable()) {
return candidate;
}
}
log.warn("Could not find relative file '" + path + "' in locations: " + Arrays.toString(locations.getLocations()));
return null;
}

// 2. Fallback to absolute path or standard resource loading
Resource resource = this.resourceLoader.getResource("file:" + path);
if (!resource.exists()) {
resource = this.resourceLoader.getResource(path);
}
return resource;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ org.springframework.cloud.config.server.config.RsaEncryptionAutoConfiguration
org.springframework.cloud.config.server.config.DefaultTextEncryptionAutoConfiguration
org.springframework.cloud.config.server.config.EncryptionAutoConfiguration
org.springframework.cloud.config.server.config.VaultEncryptionAutoConfiguration
org.springframework.cloud.config.server.config.FileResolvingEnvironmentRepositoryConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.config.server.config;

import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.cloud.config.server.environment.FileResolvingEnvironmentRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

/**
* Tests for {@link FileResolvingEnvironmentRepositoryConfiguration}.
*/
class FileResolvingEnvironmentRepositoryConfigurationTests {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(FileResolvingEnvironmentRepositoryConfiguration.class));

@Test
void shouldNotConfigureByDefault() {
this.contextRunner
.withUserConfiguration(MockRepositoryConfiguration.class)
.run(context -> assertThat(context).doesNotHaveBean(FileResolvingEnvironmentRepository.class));
}

@Test
void shouldNotConfigureIfExplicitlyDisabled() {
this.contextRunner
.withUserConfiguration(MockRepositoryConfiguration.class)
.withPropertyValues("spring.cloud.config.server.file-resolving.enabled=false")
.run(context -> assertThat(context).doesNotHaveBean(FileResolvingEnvironmentRepository.class));
}

@Test
void shouldConfigureIfExplicitlyEnabled() {
this.contextRunner
.withUserConfiguration(MockRepositoryConfiguration.class)
.withPropertyValues("spring.cloud.config.server.file-resolving.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(FileResolvingEnvironmentRepository.class);
assertThat(context.getBean(EnvironmentRepository.class))
.isInstanceOf(FileResolvingEnvironmentRepository.class);
});
}

@Test
void shouldNotConfigureIfDelegateIsMissing() {
this.contextRunner
.withPropertyValues("spring.cloud.config.server.file-resolving.enabled=true")
.run(context -> assertThat(context).doesNotHaveBean(FileResolvingEnvironmentRepository.class));
}

@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfiguration {

@Bean
EnvironmentRepository environmentRepository() {
return mock(EnvironmentRepository.class);
}

}

}
Loading
Loading