Support AWS-EC2 authentication

We now support AWS-EC2 authentication for applications running on EC2 instances. This authentication method uses the PKCS7 identity document to authenticate against Vault. It provides support for nonce (enabled by default) and roles.

fixes gh-17
This commit is contained in:
Mark Paluch
2016-07-04 22:41:15 +02:00
parent 232aae9ba8
commit 5475f58bf1
6 changed files with 295 additions and 13 deletions

View File

@@ -74,10 +74,10 @@ backend is enabled which accesses secret config settings via JSON endpoints.
The HTTP service has resources in the form:
----
/secret/{application}
/secret/{application}/{profile}
/secret/{defaultContext}
/secret/{application}
/secret/{defaultContext}/{profile}
/secret/{defaultContext}
----
where the "application" is injected as the `spring.application.name` in the

View File

@@ -125,6 +125,63 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism {
}
----
=== AWS-EC2 authentication
The https://www.vaultproject.io/docs/auth/aws-ec2.html[aws-ec2]
auth backend provides a secure introduction mechanism
for AWS EC2 instances, allowing automated retrieval of a Vault
token. Unlike most Vault authentication backends, this backend
does not require first-deploying, or provisioning security-sensitive
credentials (tokens, username/password, client certificates, etc).
Instead, it treats AWS as a Trusted Third Party and uses the
cryptographically signed dynamic metadata information that uniquely
represents each EC2 instance.
[source,yaml]
.bootstrap.yml using AWS-EC2 Authentication
----
spring.cloud.vault:
enabled: true
authentication: AWS_EC2
----
AWS-EC2 authentication enables nonce by default to follow
the Trust On First Use (TOFU) principle. Any unintended party that
gains access to the PKCS#7 signed identity metadata can authenticate
against Vault.
During the first login, Spring Cloud Vault generates a nonce
that is stored in the auth backend aside the instance Id.
Re-authentication requires the same nonce to be sent. Any other
party does not have the nonce and can raise an alert in Vault for
further investigation.
The nonce is kept in memory and is lost during application restart.
[source,yaml]
.bootstrap.yml with disabled nonce
----
spring.cloud.vault:
enabled: true
authentication: AWS_EC2
aws-ec2:
use-nonce: false
----
AWS-EC2 authentication roles are optional and default to the AMI.
You can configure the authentication role by setting the
`spring.cloud.vault.aws-ec2.role` property.
[source,yaml]
.bootstrap.yml with configured role
----
spring.cloud.vault:
enabled: true
authentication: AWS_EC2
aws-ec2:
role: application-server
----
== Backends
[[vault-client-generic]]

View File

@@ -116,6 +116,12 @@ class VaultPropertySource extends EnumerablePropertySource<VaultClient> {
return vaultState.getToken();
}
if (vaultProperties.getAuthentication() == AuthenticationMethod.AWS_EC2) {
vaultState.setToken(clientAuthentication.login());
return vaultState.getToken();
}
if (vaultProperties.getAuthentication() == AuthenticationMethod.APPID) {
AppIdProperties appIdProperties = vaultProperties.getAppId();

View File

@@ -19,11 +19,13 @@ import static org.springframework.cloud.vault.VaultClient.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
@@ -41,16 +43,16 @@ class DefaultClientAuthentication extends ClientAuthentication {
private final VaultProperties properties;
private final RestTemplate restTemplate;
private final AppIdUserIdMechanism appIdUserIdMechanism;
private char[] nonce;
/**
* Creates a {@link DefaultClientAuthentication} using {@link VaultProperties} and
* {@link RestTemplate}.
*
* @param properties must not be {@literal null}
* @param restTemplate must not be {@literal null}
*
* @param properties must not be {@literal null}.
* @param restTemplate must not be {@literal null}.
*/
public DefaultClientAuthentication(VaultProperties properties,
RestTemplate restTemplate) {
DefaultClientAuthentication(VaultProperties properties, RestTemplate restTemplate) {
Assert.notNull(properties, "VaultProperties must not be null");
Assert.notNull(restTemplate, "RestTemplate must not be null");
@@ -64,12 +66,12 @@ class DefaultClientAuthentication extends ClientAuthentication {
* Creates a {@link DefaultClientAuthentication} using {@link VaultProperties} and
* {@link RestTemplate} for AppId authentication.
*
* @param properties must not be {@literal null}
* @param restTemplate must not be {@literal null}
* @param appIdUserIdMechanism must not be {@literal null}
* @param properties must not be {@literal null}.
* @param restTemplate must not be {@literal null}.
* @param appIdUserIdMechanism must not be {@literal null}.
*/
public DefaultClientAuthentication(VaultProperties properties,
RestTemplate restTemplate, AppIdUserIdMechanism appIdUserIdMechanism) {
DefaultClientAuthentication(VaultProperties properties, RestTemplate restTemplate,
AppIdUserIdMechanism appIdUserIdMechanism) {
Assert.notNull(properties, "VaultProperties must not be null");
Assert.notNull(restTemplate, "RestTemplate must not be null");
@@ -85,11 +87,20 @@ class DefaultClientAuthentication extends ClientAuthentication {
if (properties.getAuthentication() == VaultProperties.AuthenticationMethod.APPID
&& appIdUserIdMechanism != null) {
log.info("Using AppId authentication to log into Vault");
VaultProperties.AppIdProperties appId = properties.getAppId();
return createTokenUsingAppId(new AppIdTuple(properties.getApplicationName(),
appIdUserIdMechanism.createUserId()), appId);
}
if (properties
.getAuthentication() == VaultProperties.AuthenticationMethod.AWS_EC2) {
log.info("Using AWS-EC2 authentication to log into Vault");
return createTokenUsingAwsEc2();
}
throw new UnsupportedOperationException(
String.format("Cannot create a token for auth method %s",
properties.getAuthentication()));
@@ -128,6 +139,11 @@ class DefaultClientAuthentication extends ClientAuthentication {
String.format("Cannot login using app-id: %s",
VaultErrorMessage.getError(e.getResponseBodyAsString())));
}
if (e.getStatusCode().equals(HttpStatus.BAD_REQUEST)) {
throw new IllegalStateException(
String.format("Cannot login using app-id: %s",
VaultErrorMessage.getError(e.getResponseBodyAsString())));
}
throw e;
}
@@ -141,6 +157,87 @@ class DefaultClientAuthentication extends ClientAuthentication {
return login;
}
@SuppressWarnings("unchecked")
private VaultToken createTokenUsingAwsEc2() {
VaultProperties.AwsEc2Properties properties = this.properties.getAwsEc2();
String url = buildUrl();
Map<String, String> variables = new HashMap<>();
variables.put("backend", "auth/" + properties.getAwsEc2Path());
variables.put("key", "login");
try {
Map<String, String> login = getEc2Login(properties);
ResponseEntity<VaultResponse> response = restTemplate.postForEntity(url,
new HttpEntity<>(login), VaultResponse.class, variables);
HttpStatus status = response.getStatusCode();
if (!status.is2xxSuccessful()) {
throw new IllegalStateException("Cannot login using AWS-EC2");
}
VaultResponse body = response.getBody();
String token = (String) body.getAuth().get("client_token");
if (log.isDebugEnabled()) {
if (body.getAuth().get("metadata") instanceof Map) {
Map<Object, Object> metadata = (Map<Object, Object>) body.getAuth()
.get("metadata");
log.debug(String.format(
"Login successful using AWS-EC2 authentication for instance %s, AMI %s",
metadata.get("instance_id"), metadata.get("instance_id")));
}
else {
log.debug("Login successful using AWS-EC2 authentication");
}
}
return VaultToken.of(token, body.getLeaseDuration());
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode().equals(HttpStatus.BAD_REQUEST)) {
throw new IllegalStateException(
String.format("Cannot login using AWS-EC2: %s",
VaultErrorMessage.getError(e.getResponseBodyAsString())));
}
throw e;
}
}
private Map<String, String> getEc2Login(VaultProperties.AwsEc2Properties properties) {
Map<String, String> login = new HashMap<>();
if (StringUtils.hasText(properties.getRole())) {
login.put("role", properties.getRole());
}
if (properties.isUseNonce()) {
if (this.nonce == null) {
this.nonce = createNonce();
}
login.put("nonce", new String(this.nonce));
}
String pkcs7 = restTemplate.getForObject(properties.getIdentityDocument(),
String.class);
if (StringUtils.hasText(pkcs7)) {
login.put("pkcs7", pkcs7.replaceAll("\\r", "").replace("\\n", ""));
}
return login;
}
private char[] createNonce() {
return UUID.randomUUID().toString().toCharArray();
}
@Value
private static class AppIdTuple {
private String appId;

View File

@@ -74,6 +74,8 @@ public class VaultProperties {
private AppIdProperties appId = new AppIdProperties();
private AwsEc2Properties awsEc2 = new AwsEc2Properties();
private Ssl ssl = new Ssl();
/**
@@ -89,12 +91,14 @@ public class VaultProperties {
/**
* Property value for UserId generation using a Mac-Address.
*
* @see MacAddressUserId
*/
public final static String MAC_ADDRESS = "MAC_ADDRESS";
/**
* Property value for UserId generation using an IP-Address.
*
* @see IpAddressUserId
*/
public final static String IP_ADDRESS = "IP_ADDRESS";
@@ -117,6 +121,32 @@ public class VaultProperties {
private String userId = MAC_ADDRESS;
}
@Data
public static class AwsEc2Properties {
/**
* URL of the AWS-EC2 PKCS7 identity document.
*/
@NotEmpty
private String identityDocument = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7";
/**
* Mount path of the AWS-EC2 authentication backend.
*/
@NotEmpty
private String awsEc2Path = "aws-ec2";
/**
* Name of the role, optional.
*/
private String role = "";
/**
* Flag whether to generate and send a nonce.
*/
private boolean useNonce = true;
}
@Data
public static class Ssl {
@@ -132,6 +162,6 @@ public class VaultProperties {
}
public enum AuthenticationMethod {
TOKEN, APPID,
TOKEN, APPID, AWS_EC2,
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2016 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.vault;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.util.StringUtils;
/**
* Integration tests for {@link VaultClient} using AWS-EC2 login. This test requires AWS
* credentials, a region and an AMI, see {@link #AWS_ACCESS_KEY}, {@link #AWS_SECRET_KEY}
* and the {@link IntegrationTest} properties to be provided externally. It needs to be
* run on a AWS-EC2 instance to be able to obtain instance metadata.
*
* @author Mark Paluch
*/
public class AwsEc2AuthenticationMethodsIntegrationTests
extends AbstractIntegrationTests {
private final static String AWS_REGION = "eu-west-1";
private final static String AWS_AMI = "ami-f95ef58a";
private final static String AWS_ACCESS_KEY = System.getProperty("aws.access.key");
private final static String AWS_SECRET_KEY = System.getProperty("aws.secret.key");
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before
public void setUp() throws Exception {
assumeTrue(StringUtils.hasText(AWS_ACCESS_KEY)
&& StringUtils.hasText(AWS_SECRET_KEY));
if (!prepare().hasAuth("aws-ec2")) {
prepare().mountAuth("aws-ec2");
}
Map<String, String> config = new HashMap<>();
config.put("access_key", AWS_ACCESS_KEY);
config.put("secret_key", AWS_SECRET_KEY);
config.put("endpoint", String.format("https://ec2.%s.amazonaws.com", AWS_REGION));
prepare().write("auth/aws-ec2/config/client", config);
prepare().write(String.format("auth/aws-ec2/role/%s", AWS_AMI),
Collections.singletonMap("bound_ami_id", AWS_AMI));
}
@Test
public void loginShouldCreateAToken() throws Exception {
VaultProperties vaultProperties = prepareAwsEc2Authentication();
ClientAuthentication clientAuthentication = new DefaultClientAuthentication(
vaultProperties, TestRestTemplateFactory.create(vaultProperties));
assertThat(clientAuthentication.login()).isNotNull();
}
private VaultProperties prepareAwsEc2Authentication() {
VaultProperties vaultProperties = Settings.createVaultProperties();
vaultProperties.setAuthentication(AuthenticationMethod.AWS_EC2);
return vaultProperties;
}
}