Add HTTP Basic security
This commit is contained in:
@@ -14,6 +14,20 @@ configuration (name-value pairs, or equivalent YAML content). The
|
||||
server is easily embeddable in a Spring Boot application using the
|
||||
`@EnableConfigServer` annotation.
|
||||
|
||||
=== Security
|
||||
|
||||
You are free to secure your Config Server in any way that makes sense
|
||||
to you (from physical network security to OAuth2 bearer
|
||||
tokens), and Spring Security and Spring Boot make it easy to do pretty
|
||||
much anything.
|
||||
|
||||
To use the default Spring Boot configured HTTP Basic security, just
|
||||
include Spring Security on the classpath (e.g. through
|
||||
`spring-boot-starter-security`). The default is a username of "user"
|
||||
and a randomly generated password, which isn't going to be very useful
|
||||
in practice, so we recommend you configure the password (via
|
||||
`security.user.password`) and encrypt it (see below for instructions
|
||||
on how to do that).
|
||||
|
||||
=== Encryption and Decryption
|
||||
|
||||
@@ -29,7 +43,9 @@ agents). If the remote property sources contain encryted content
|
||||
(values starting with `{cipher}`) they will be decrypted before
|
||||
sending to clients over HTTP. The main advantage of this set up is
|
||||
that the property values don't have to be in plain text when they are
|
||||
"at rest" (e.g. in a git repository).
|
||||
"at rest" (e.g. in a git repository). If a value cannot be decrypted
|
||||
it is replaced with an empty string, largely to prevent cipher text
|
||||
being used as a password in Spring Boot autconfigured HTTP basic.
|
||||
|
||||
If you are setting up a remote config repository for config client
|
||||
applications it might contain an `application.yml` like this, for
|
||||
@@ -148,6 +164,7 @@ client starts up it binds to the Config Server (via the bootstrap
|
||||
configuration property `spring.cloud.config.uri`) and initializes
|
||||
Spring `Environment` with remote property sources
|
||||
|
||||
|
||||
=== Environment Changes
|
||||
|
||||
The application will listen for an `EnvironmentChangedEvent` and react
|
||||
@@ -297,3 +314,38 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomP
|
||||
|
||||
then the "customProperty" `PropertySource` will show up in any
|
||||
application that includes that jar on its classpath.
|
||||
|
||||
=== Security
|
||||
|
||||
If you use HTTP Basic security on the server then clients just need to
|
||||
know the password (and username if it isn't the default). You can do
|
||||
that via the config server URI, or via separate username and password
|
||||
properties, e.g.
|
||||
|
||||
.bootstrap.yml
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
config:
|
||||
uri: https://user:secret@myconfig.mycompany.com
|
||||
----
|
||||
|
||||
or
|
||||
|
||||
.bootstrap.yml
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
config:
|
||||
uri: https://myconfig.mycompany.com
|
||||
username: user
|
||||
password: secret
|
||||
----
|
||||
|
||||
The `spring.cloud.config.password` and `spring.cloud.config.username`
|
||||
values override anything that is provided in the URI.
|
||||
|
||||
If you use another form of security you might need to provide a
|
||||
`RestTemplate` to the `ConfigServicePropertySourceLocator` (e.g. by
|
||||
grabbing it in the bootstrap context and injecting one).
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-rsa</artifactId>
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration.KeyCondition;
|
||||
import org.springframework.cloud.bootstrap.encrypt.KeyProperties.KeyStore;
|
||||
import org.springframework.cloud.config.encrypt.EncryptorFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -42,11 +41,14 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(TextEncryptor.class)
|
||||
@Conditional(KeyCondition.class)
|
||||
@EnableConfigurationProperties(KeyProperties.class)
|
||||
public class EncryptionBootstrapConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
private TextEncryptor encryptor;
|
||||
|
||||
@Configuration
|
||||
@Conditional(KeyCondition.class)
|
||||
@ConditionalOnClass(RsaSecretEncryptor.class)
|
||||
protected static class RsaEncryptionConfiguration {
|
||||
|
||||
@@ -57,7 +59,7 @@ public class EncryptionBootstrapConfiguration {
|
||||
@ConditionalOnMissingBean(TextEncryptor.class)
|
||||
public TextEncryptor textEncryptor() {
|
||||
KeyStore keyStore = key.getKeyStore();
|
||||
if (keyStore.getLocation()!=null && keyStore.getLocation().exists()) {
|
||||
if (keyStore.getLocation() != null && keyStore.getLocation().exists()) {
|
||||
return new RsaSecretEncryptor(
|
||||
new KeyStoreKeyFactory(keyStore.getLocation(), keyStore
|
||||
.getPassword().toCharArray()).getKeyPair(keyStore
|
||||
@@ -69,6 +71,7 @@ public class EncryptionBootstrapConfiguration {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Conditional(KeyCondition.class)
|
||||
@ConditionalOnMissingClass(name = "org.springframework.security.rsa.crypto.RsaSecretEncryptor")
|
||||
protected static class VanillaEncryptionConfiguration {
|
||||
|
||||
@@ -84,8 +87,10 @@ public class EncryptionBootstrapConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EnvironmentDecryptApplicationListener environmentDecryptApplicationListener(
|
||||
TextEncryptor encryptor) {
|
||||
public EnvironmentDecryptApplicationListener environmentDecryptApplicationListener() {
|
||||
if (encryptor == null) {
|
||||
encryptor = new FailsafeTextEncryptor();
|
||||
}
|
||||
return new EnvironmentDecryptApplicationListener(encryptor);
|
||||
}
|
||||
|
||||
@@ -118,4 +123,27 @@ public class EncryptionBootstrapConfiguration {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* TextEncryptor that just fails, so that users don't get a false sense of security
|
||||
* adding ciphers to config files and not getting them decrypted.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
protected static class FailsafeTextEncryptor implements TextEncryptor {
|
||||
|
||||
@Override
|
||||
public String encrypt(String text) {
|
||||
throw new UnsupportedOperationException(
|
||||
"No encryption for FailsafeTextEncryptor. Did you configure the keystore correctly?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String encryptedText) {
|
||||
throw new UnsupportedOperationException(
|
||||
"No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -94,11 +94,20 @@ public class EnvironmentDecryptApplicationListener implements
|
||||
value = value.substring("{cipher}".length());
|
||||
try {
|
||||
value = encryptor.decrypt(value);
|
||||
overrides.put(key, value);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Decrypted: key=" + key);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.warn("Cannot decrypt: key=" + key);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn("Cannot decrypt: key=" + key, e);
|
||||
} else {
|
||||
logger.warn("Cannot decrypt: key=" + key);
|
||||
}
|
||||
// Set value to empty to avoid making a password out of the cipher text
|
||||
value = "";
|
||||
}
|
||||
overrides.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,17 +16,28 @@
|
||||
|
||||
package org.springframework.cloud.config.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.config.Environment;
|
||||
import org.springframework.cloud.config.PropertySource;
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.cloud.config.Environment;
|
||||
import org.springframework.cloud.config.PropertySource;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.security.crypto.codec.Base64;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
@@ -42,14 +53,19 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
|
||||
|
||||
private String label = "master";
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private String uri = "http://localhost:8888";
|
||||
|
||||
private RestTemplate restTemplate = new RestTemplate();
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Override
|
||||
public org.springframework.core.env.PropertySource<?> locate() {
|
||||
CompositePropertySource composite = new CompositePropertySource("configService");
|
||||
Environment result = restTemplate.exchange(uri + "/{name}/{env}/{label}",
|
||||
RestTemplate restTemplate = this.restTemplate==null ? getSecureRestTemplate() : this.restTemplate;
|
||||
Environment result = restTemplate.exchange(getUri() + "/{name}/{env}/{label}",
|
||||
HttpMethod.GET, new HttpEntity<Void>((Void) null), Environment.class,
|
||||
name, env, label).getBody();
|
||||
for (PropertySource source : result.getPropertySources()) {
|
||||
@@ -60,8 +76,12 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
|
||||
return composite;
|
||||
}
|
||||
|
||||
public void setRestTemplate(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
return extractCredentials()[2];
|
||||
}
|
||||
|
||||
public void setUri(String url) {
|
||||
@@ -92,4 +112,98 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return extractCredentials()[0];
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return extractCredentials()[1];
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
private RestTemplate getSecureRestTemplate() {
|
||||
RestTemplate template = new RestTemplate();
|
||||
String[] userInfo = extractCredentials();
|
||||
if (userInfo[1]!=null) {
|
||||
template.setInterceptors(Arrays
|
||||
.<ClientHttpRequestInterceptor> asList(new BasicAuthorizationInterceptor(
|
||||
userInfo[0], userInfo[1])));
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
private String[] extractCredentials() {
|
||||
String[] result = new String[3];
|
||||
String uri = this.uri;
|
||||
result[2] = uri;
|
||||
String[] creds = getUsernamePassword();
|
||||
result[0] = creds[0];
|
||||
result[1] = creds[1];
|
||||
try {
|
||||
URL url = new URL(uri);
|
||||
String userInfo = url.getUserInfo();
|
||||
if (StringUtils.isEmpty(userInfo) || ":".equals(userInfo)) {
|
||||
return result;
|
||||
}
|
||||
String bare = UriComponentsBuilder.fromHttpUrl(uri).userInfo(null).build()
|
||||
.toUriString();
|
||||
result[2] = bare;
|
||||
if (!userInfo.contains(":")) {
|
||||
userInfo = userInfo + ":";
|
||||
}
|
||||
String[] split = userInfo.split(":");
|
||||
result[0] = split[0];
|
||||
result[1] = split[1];
|
||||
if (creds[1]!=null) {
|
||||
// Explicit username / password takes precedence
|
||||
result[1] = creds[1];
|
||||
if ("user".equals(creds[0])) {
|
||||
// But the username can be overridden
|
||||
result[0] = split[0];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Invalid URL: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] getUsernamePassword() {
|
||||
if (StringUtils.hasText(password)) {
|
||||
return new String[] {StringUtils.hasText(username) ? username.trim() : "user", password.trim()};
|
||||
}
|
||||
return new String[2];
|
||||
}
|
||||
|
||||
private static class BasicAuthorizationInterceptor implements
|
||||
ClientHttpRequestInterceptor {
|
||||
|
||||
private final String username;
|
||||
|
||||
private final String password;
|
||||
|
||||
public BasicAuthorizationInterceptor(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = (password == null ? "" : password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
byte[] token = Base64
|
||||
.encode((this.username + ":" + this.password).getBytes());
|
||||
request.getHeaders().add("Authorization", "Basic " + new String(token));
|
||||
return execution.execute(request, body);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2013-2014 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
|
||||
*
|
||||
* http://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.client;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class ConfigServicePropertySourceLocatorTests {
|
||||
|
||||
private ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator();
|
||||
|
||||
@Test
|
||||
public void vanilla() {
|
||||
locator.setUri("http://localhost:9999");
|
||||
locator.setPassword("secret");
|
||||
assertEquals("http://localhost:9999", locator.getUri());
|
||||
assertEquals("user", locator.getUsername());
|
||||
assertEquals("secret", locator.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriCreds() {
|
||||
locator.setUri("http://foo:bar@localhost:9999");
|
||||
assertEquals("http://localhost:9999", locator.getUri());
|
||||
assertEquals("foo", locator.getUsername());
|
||||
assertEquals("bar", locator.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overridePassword() {
|
||||
locator.setUri("http://foo:bar@localhost:9999");
|
||||
locator.setPassword("secret");
|
||||
assertEquals("http://localhost:9999", locator.getUri());
|
||||
assertEquals("foo", locator.getUsername());
|
||||
assertEquals("secret", locator.getPassword());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class ConfigServerConfiguration {
|
||||
private ConfigurableEnvironment environment;
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties("spring.platform.config.server")
|
||||
@ConfigurationProperties("spring.cloud.config.server")
|
||||
public JGitEnvironmentRepository repository() {
|
||||
return new JGitEnvironmentRepository(environment);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user