Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
Merge pull request #36 from Nike-Inc/ecs-support
Browse files Browse the repository at this point in the history
* add support for ECS
  • Loading branch information
sdford authored May 17, 2018
2 parents 3337504 + eeea72e commit bb02d72
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ See `DefaultCerberusCredentialsProviderChain.java` for full usage.
If the client library is running on an EC2 instance, it will attempt to use the instance's assigned IAM role to authenticate
with Cerberus and obtain a token.

If the client library is running in an ECS task, it will attempt to use the task's execution IAM role to authenticate
with Cerberus and obtain a token.

The IAM role must be configured for access to Cerberus before this will work.

The following policy statement must also be assigned to the IAM role, so that the client can automatically decrypt the auth token from the Cerberus IAM auth endpoint:
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
version=5.1.0
version=5.2.0
groupId=com.nike
artifactId=cerberus-client
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.nike.cerberus.client.DefaultCerberusUrlResolver;
import com.nike.cerberus.client.UrlResolver;
import com.nike.cerberus.client.auth.aws.EcsTaskRoleCerberusCredentialsProvider;
import com.nike.cerberus.client.auth.aws.InstanceRoleCerberusCredentialsProvider;
import okhttp3.OkHttpClient;

Expand Down Expand Up @@ -51,7 +52,8 @@ public DefaultCerberusCredentialsProviderChain() {
public DefaultCerberusCredentialsProviderChain(UrlResolver urlResolver) {
super(new EnvironmentCerberusCredentialsProvider(),
new SystemPropertyCerberusCredentialsProvider(),
new InstanceRoleCerberusCredentialsProvider(urlResolver));
new InstanceRoleCerberusCredentialsProvider(urlResolver),
new EcsTaskRoleCerberusCredentialsProvider(urlResolver));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright (c) 2018 Nike, Inc.
*
* 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 com.nike.cerberus.client.auth.aws;

import com.amazonaws.AmazonClientException;
import com.amazonaws.internal.EC2CredentialsUtils;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.util.json.Jackson;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonSyntaxException;
import com.nike.cerberus.client.CerberusClientException;
import com.nike.cerberus.client.UrlResolver;
import com.nike.cerberus.client.auth.CerberusCredentialsProvider;
import okhttp3.OkHttpClient;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
* {@link CerberusCredentialsProvider} implementation that uses the assigned role
* to an ECS task to authenticate with Cerberus and decrypt the auth
* response using KMS. If the assigned role has been granted the appropriate
* provisioned for usage of Cerberus, it will succeed and have a token that can be
* used to interact with Cerberus.
* <p>
* This class uses the AWS Task Metadata endpoint to look-up information automatically.
*
* @see <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint.html">Amazon ECS Task Metadata Endpoint</a>
*/
public class EcsTaskRoleCerberusCredentialsProvider extends BaseAwsCredentialsProvider {

private static final Logger LOGGER = LoggerFactory.getLogger(EcsTaskRoleCerberusCredentialsProvider.class);

private static final Pattern TASK_ARN_PATTERN = Pattern.compile("arn:aws:ecs:(?<region>.*):(.*):task/(.*)");

/** The name of the Json Object that contains the role ARN.*/
final String ROLE_ARN = "RoleArn";

/** Environment variable to get the Amazon ECS credentials resource path. */
private static final String ECS_CONTAINER_CREDENTIALS_PATH = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";

private static final String ECS_TASK_METADATA_RELATIVE_URI = "/v2/metadata";

/** Default endpoint to retrieve the Amazon ECS Credentials and metadata. */
private static final String ECS_CREDENTIALS_ENDPOINT = "http://169.254.170.2";

/**
* Constructor to setup credentials provider using the specified
* implementation of {@link UrlResolver}
*
* @param urlResolver Resolver for resolving the Cerberus URL
*/
public EcsTaskRoleCerberusCredentialsProvider(UrlResolver urlResolver) {
super(urlResolver);
}

/**
* Constructor to setup credentials provider using the specified
* implementation of {@link UrlResolver} and {@link OkHttpClient}
*
* @param urlResolver Resolver for resolving the Cerberus URL
* @param httpClient the client for interacting with Cerberus
*/
public EcsTaskRoleCerberusCredentialsProvider(UrlResolver urlResolver, OkHttpClient httpClient) {
super(urlResolver, httpClient);
}

/**
* Constructor to setup credentials provider using the specified
* implementation of {@link UrlResolver}
*
* @param urlResolver Resolver for resolving the Cerberus URL
* @param xCerberusClientOverride Overrides the default header value for the 'X-Cerberus-Client' header
*/
public EcsTaskRoleCerberusCredentialsProvider(UrlResolver urlResolver, String xCerberusClientOverride) {
super(urlResolver, xCerberusClientOverride);
}

/**
* Looks up the IAM roles assigned to the task via the ECS task metadata
* service. An attempt is made to authenticate and decrypt the Cerberus
* auth response with KMS using the task execution role. If successful,
* the token retrieved is cached locally for future calls to
* {@link BaseAwsCredentialsProvider#getCredentials()}.
*/
@Override
protected void authenticate() {
String roleArn = getRoleArn();
Region region = getRegion();

try {
getAndSetToken(roleArn, region);
return;
} catch (AmazonClientException ace) {
LOGGER.warn("Unexpected error communicating with AWS services.", ace);
} catch (JsonSyntaxException jse) {
LOGGER.error("The decrypted auth response was not in the expected format!", jse);
} catch (CerberusClientException sce) {
LOGGER.warn("Unable to acquire Cerberus token for IAM role: " + roleArn, sce);
}

throw new CerberusClientException("Unable to acquire token with ECS task execution role.");
}

private String getRoleArn(){
JsonNode node;
JsonNode roleArn;
try {
String credentialsResponse = EC2CredentialsUtils.getInstance().readResource(
getCredentialsEndpoint());

node = Jackson.jsonNodeOf(credentialsResponse);
roleArn = node.get(ROLE_ARN);
if (roleArn == null){
throw new CerberusClientException("Task execution role ARN not found in task credentials.");
}
return roleArn.asText();
} catch (JsonMappingException e) {
LOGGER.error("Unable to parse response returned from service endpoint", e);
} catch (IOException e) {
LOGGER.error("Unable to load credentials from service endpoint", e);
} catch (AmazonClientException ace) {
LOGGER.warn("Unexpected error communicating with AWS services.", ace);
}
throw new CerberusClientException("Unable to find task execution role ARN.");
}

private Region getRegion(){
try {
String credentialsResponse = EC2CredentialsUtils.getInstance().readResource(getMetadataEndpoint());
JsonNode node = Jackson.jsonNodeOf(credentialsResponse);
JsonNode taskArn = node.get("TaskARN");
final Matcher matcher = TASK_ARN_PATTERN.matcher(taskArn.asText());

if (matcher.matches()) {
final String region = matcher.group("region");
if (StringUtils.isNotBlank(region)) {
return Region.getRegion(Regions.fromName(region));
} else {
LOGGER.warn("Cannot parse region from task ARN {}", taskArn.asText());
}
}
} catch (IOException e) {
LOGGER.warn("Unable to read resource from the task metadata endpoint.", e);
} catch (URISyntaxException e) {
LOGGER.warn(ECS_CREDENTIALS_ENDPOINT + ECS_TASK_METADATA_RELATIVE_URI + " could not be parsed as a URI reference.");
} catch (RuntimeException e) {
LOGGER.warn("Region lookup failed", e);
}
LOGGER.info("Using default region as fallback.");
return Region.getRegion(Regions.DEFAULT_REGION);
}

private URI getMetadataEndpoint() throws URISyntaxException {
return new URI(ECS_CREDENTIALS_ENDPOINT + ECS_TASK_METADATA_RELATIVE_URI);
}


private URI getCredentialsEndpoint(){
String path = System.getenv(ECS_CONTAINER_CREDENTIALS_PATH);
if (path == null) {
throw new CerberusClientException("The environment variable " + ECS_CONTAINER_CREDENTIALS_PATH + " is empty");
}
try {
return new URI(ECS_CREDENTIALS_ENDPOINT + path);
} catch (URISyntaxException e) {
throw new CerberusClientException(ECS_CREDENTIALS_ENDPOINT + path + " could not be parsed as a URI reference.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2018 Nike, Inc.
*
* 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 com.nike.cerberus.client.auth.aws;

import com.amazonaws.AmazonClientException;
import com.amazonaws.internal.EC2CredentialsUtils;
import com.amazonaws.services.kms.AWSKMSClient;
import com.nike.cerberus.client.CerberusClientException;
import com.nike.cerberus.client.DefaultCerberusUrlResolver;
import com.nike.cerberus.client.UrlResolver;
import com.nike.cerberus.client.auth.CerberusCredentials;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.io.IOException;
import java.net.URI;

import static org.assertj.core.api.Assertions.assertThat;
import static org.powermock.api.mockito.PowerMockito.*;

/**
* Tests the EcsTaskRoleCerberusCredentialsProvider class
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({AWSKMSClient.class,
EcsTaskRoleCerberusCredentialsProvider.class, EC2CredentialsUtils.class})
@PowerMockIgnore({"javax.management.*","javax.net.*"})
public class EcsTaskExecutionRoleCerberusCredentialsProviderTest extends BaseCredentialsProviderTest {

private UrlResolver urlResolver;

private AWSKMSClient kmsClient;

private EcsTaskRoleCerberusCredentialsProvider provider;

private EC2CredentialsUtils ec2CredentialsUtils;

@Before
public void setup() throws Exception {
kmsClient = mock(AWSKMSClient.class);
urlResolver = mock(UrlResolver.class);
provider = new EcsTaskRoleCerberusCredentialsProvider(urlResolver);

whenNew(AWSKMSClient.class).withAnyArguments().thenReturn(kmsClient);
mockStatic(System.class);
mockGetCredentialsRelativeUri();
mockStatic(EC2CredentialsUtils.class);
ec2CredentialsUtils = mock(EC2CredentialsUtils.class);
when(EC2CredentialsUtils.getInstance()).thenReturn(ec2CredentialsUtils);
}

@Test
public void getCredentials_returns_valid_credentials() throws IOException {

MockWebServer mockWebServer = new MockWebServer();
mockWebServer.start();
final String cerberusUrl = "http://localhost:" + mockWebServer.getPort();

mockDecrypt(kmsClient, DECODED_AUTH_DATA);
when(urlResolver.resolve()).thenReturn(cerberusUrl);

System.setProperty(DefaultCerberusUrlResolver.CERBERUS_ADDR_SYS_PROPERTY, cerberusUrl);
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(AUTH_RESPONSE));

when(ec2CredentialsUtils.readResource(Mockito.any(URI.class)))
.thenReturn("{\"RoleArn\":\"arn:aws:iam::123456789:role/ecsTaskExecutionRole\"}")
.thenReturn("{\"TaskARN\":\"arn:aws:ecs:us-west-1:123456789:task/task-id\"}");

CerberusCredentials credentials = provider.getCredentials();
assertThat(credentials.getToken()).isEqualTo(AUTH_TOKEN);

}

@Test(expected = CerberusClientException.class)
public void getCredentials_throws_client_exception_when_task_arn_is_missing() throws IOException {
when(ec2CredentialsUtils.readResource(Mockito.any(URI.class)))
.thenReturn("{}")
.thenReturn("{\"TaskARN\":\"arn:aws:ecs:us-west-1:123456789:task/task-id\"}");
provider.getCredentials();
}

@Test(expected = CerberusClientException.class)
public void getCredentials_throws_client_exception_when_not_running_in_ecs_task() throws IOException{
when(ec2CredentialsUtils.readResource(Mockito.any(URI.class))).thenThrow(new AmazonClientException("BAD"));
provider.getCredentials();
}

private void mockGetCredentialsRelativeUri() {
when(System.getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")).thenReturn("/mockuri");
}
}

0 comments on commit bb02d72

Please sign in to comment.