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:
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user