Add HTTP Basic security

This commit is contained in:
Dave Syer
2014-10-10 15:57:50 +01:00
parent b6ba221aea
commit 5d15afabe5
7 changed files with 277 additions and 14 deletions

View File

@@ -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).

View File

@@ -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>

View File

@@ -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?");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}