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 #1 from Nike-Inc/feature/aws-lambda-func-auth
Browse files Browse the repository at this point in the history
Authenticate with Cerberus from an AWS Lambda function
  • Loading branch information
sdford authored Nov 29, 2016
2 parents 447c05f + 768291b commit 4f51ce3
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 65 deletions.
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ This library acts as a wrapper around the Nike developed Vault client by configu

## Quickstart

### Default Client

``` java
final VaultClient vaultClient = DefaultCerberusClientFactory.getClient();
```

### Default URL Assumptions
#### Default URL Assumptions

The example above uses the `DefaultCerberusUrlResolver` to resolve the URL for Vault.

Expand All @@ -27,7 +29,7 @@ or the JVM system property, `cerberus.addr`, must be set:

cerberus.addr=https://cerberus

### Default Credentials Provider Assumptions
#### Default Credentials Provider Assumptions

Again, for the example above, the `DefaultCerberusCredentialsProviderChain` is used to resolve the token needed to interact with Vault.

Expand All @@ -39,13 +41,54 @@ or the JVM system property, `vault.token`, must be set:

cerberus.token=TOKEN

or the IAM role authentication flow:
or the EC2 IAM role authentication flow:

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.

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:

``` json
{
"Sid": "allow-kms-decrypt",
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": [
"*"
]
}
```

### Client that can authenticate from Lambdas

#### Prerequisites

The IAM role assigned to the Lambda function must contain the following policy statement in addition to the above KMS decrypt policy, this is so the Lambda can look up its metadata to automatically authenticate with the Cerberus IAM auth endpoint:

``` json
{
"Sid": "allow-get-function-config",
"Effect": "Allow",
"Action": [
"lambda:GetFunctionConfiguration"
],
"Resource": [
"*"
]
}
```

#### Configure the Client

``` java
final String invokedFunctionArn = context.getInvokedFunctionArn()
final VaultClient vaultClient = DefaultCerberusClientFactory.getClientForLambda(invokedFunctionArn);
```

## Further Details

Cerberus client is a small project. It only has a few classes and they are all fully documented. For further details please see the source code, including javadocs and unit tests.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
# limitations under the License.
#

version=1.0.0
version=1.1.0
groupId=com.nike
artifactId=cerberus-client
1 change: 1 addition & 0 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
compile "org.slf4j:slf4j-api:1.7.14"
compile "com.amazonaws:aws-java-sdk-core:1.10.50"
compile "com.amazonaws:aws-java-sdk-kms:1.10.50"
compile "com.amazonaws:aws-java-sdk-lambda:1.10.50"

testCompile "junit:junit:4.12"
testCompile ("org.mockito:mockito-core:1.10.19") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
package com.nike.cerberus.client;

import com.nike.cerberus.client.auth.DefaultCerberusCredentialsProviderChain;
import com.nike.cerberus.client.auth.EnvironmentCerberusCredentialsProvider;
import com.nike.cerberus.client.auth.SystemPropertyCerberusCredentialsProvider;
import com.nike.cerberus.client.auth.aws.LambdaRoleVaultCredentialsProvider;
import com.nike.vault.client.VaultClient;
import com.nike.vault.client.VaultClientFactory;
import com.nike.vault.client.auth.VaultCredentialsProviderChain;

/**
* Client factory for creating a Vault client with a URL resolver and credentials provider specific to Cerberus.
Expand All @@ -35,4 +39,21 @@ public static VaultClient getClient() {
return VaultClientFactory.getClient(new DefaultCerberusUrlResolver(),
new DefaultCerberusCredentialsProviderChain());
}

/**
* Creates a new {@link VaultClient} with the {@link DefaultCerberusUrlResolver} for URL resolving
* and a credentials provider chain that includes the {@link LambdaRoleVaultCredentialsProvider} for obtaining
* credentials.
*
* @param invokedFunctionArn The ARN for the AWS Lambda function being invoked.
* @return Vault client
*/
public static VaultClient getClientForLambda(final String invokedFunctionArn) {
final DefaultCerberusUrlResolver urlResolver = new DefaultCerberusUrlResolver();
return VaultClientFactory.getClient(urlResolver,
new VaultCredentialsProviderChain(
new EnvironmentCerberusCredentialsProvider(),
new SystemPropertyCerberusCredentialsProvider(),
new LambdaRoleVaultCredentialsProvider(urlResolver, invokedFunctionArn)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@

package com.nike.cerberus.client.auth.aws;

import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.kms.AWSKMS;
import com.amazonaws.services.kms.AWSKMSClient;
import com.amazonaws.services.kms.model.DecryptRequest;
import com.amazonaws.services.kms.model.DecryptResult;
import com.amazonaws.util.Base64;
import com.amazonaws.util.EC2MetadataUtils;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
Expand All @@ -43,6 +44,7 @@
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -56,8 +58,6 @@
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* {@link VaultCredentialsProvider} implementation that uses some AWS
Expand All @@ -72,8 +72,6 @@ public abstract class BaseAwsCredentialsProvider implements VaultCredentialsProv

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

private final Pattern iamArnPattern = Pattern.compile("(arn\\:aws\\:iam\\:\\:)(?<accountId>[0-9].*)(\\:.*)");

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

private final Lock readLock = readWriteLock.readLock();
Expand Down Expand Up @@ -139,29 +137,38 @@ public VaultCredentials getCredentials() {
abstract protected void authenticate();

/**
* Parses and returns the AWS account ID from the instance profile ARN.
* Authenticates with Cerberus and decrypts and sets the token and expiration details.
*
* @return AWS account ID
* @param accountId
* AWS account ID used to auth with cerberus
* @param iamRole
* IAM role name used to auth with cerberus
*/
protected String lookupAccountId() {
final EC2MetadataUtils.IAMInfo iamInfo = EC2MetadataUtils.getIAMInstanceProfileInfo();

if (iamInfo == null) {
final String errorMessage = "No IAM Instance Profile assigned to running instance.";
LOGGER.error(errorMessage);
throw new VaultClientException(errorMessage);
}
protected void getAndSetToken(final String accountId, final String iamRole) {
getAndSetToken(accountId, iamRole, Regions.getCurrentRegion());
}

final Matcher matcher = iamArnPattern.matcher(iamInfo.instanceProfileArn);
/**
* Authenticates with Cerberus and decrypts and sets the token and expiration details.
*
* @param accountId
* AWS account ID used to auth with cerberus
* @param iamRole
* IAM role name used to auth with cerberus
* @param region
* AWS Region used in auth with cerberus
*/
protected void getAndSetToken(final String accountId, final String iamRole, final Region region) {
final AWSKMSClient kmsClient = new AWSKMSClient();
kmsClient.setRegion(region);

if (matcher.matches()) {
final String accountId = matcher.group("accountId");
if (StringUtils.isNotBlank(accountId)) {
return accountId;
}
}
final String encryptedAuthData = getEncryptedAuthData(accountId, iamRole, region);
final VaultAuthResponse decryptedToken = decryptToken(kmsClient, encryptedAuthData);
final DateTime expires = DateTime.now(DateTimeZone.UTC);
expires.plusSeconds(decryptedToken.getLeaseDuration() - paddingTimeInSeconds);

throw new VaultClientException("Unable to obtain AWS account ID from instance profile ARN.");
credentials = new TokenVaultCredentials(decryptedToken.getClientToken());
expireDateTime = expires;
}

/**
Expand All @@ -171,9 +178,11 @@ protected String lookupAccountId() {
* AWS account ID used in the row key
* @param roleName
* IAM role name used in the row key
* @param region
* Current region of the running function or instance
* @return Base64 and encrypted token
*/
protected String getEncryptedAuthData(final String accountId, final String roleName) {
protected String getEncryptedAuthData(final String accountId, final String roleName, Region region) {
final String url = urlResolver.resolve();

if (StringUtils.isBlank(url)) {
Expand All @@ -189,7 +198,7 @@ protected String getEncryptedAuthData(final String accountId, final String roleN
Request.Builder requestBuilder = new Request.Builder().url(url + "/v1/auth/iam-role")
.addHeader(HttpHeader.ACCEPT, DEFAULT_MEDIA_TYPE.toString())
.addHeader(HttpHeader.CONTENT_TYPE, DEFAULT_MEDIA_TYPE.toString())
.method(HttpMethod.POST, buildCredentialsRequestBody(accountId, roleName));
.method(HttpMethod.POST, buildCredentialsRequestBody(accountId, roleName, region));

Response response = httpClient.newCall(requestBuilder.build()).execute();

Expand Down Expand Up @@ -240,11 +249,13 @@ protected VaultAuthResponse decryptToken(AWSKMS kmsClient, String encryptedToken
return gson.fromJson(decryptedAuthData, VaultAuthResponse.class);
}

private RequestBody buildCredentialsRequestBody(final String accountId, final String roleName) {
private RequestBody buildCredentialsRequestBody(final String accountId, final String roleName, Region region) {
final String regionName = region == null ? Regions.getCurrentRegion().getName() : region.getName();

final Map<String, String> credentials = new HashMap<>();
credentials.put("account_id", accountId);
credentials.put("role_name", roleName);
credentials.put("region", Regions.getCurrentRegion().getName());
credentials.put("region", regionName);

return RequestBody.create(DEFAULT_MEDIA_TYPE, gson.toJson(credentials));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,18 @@
package com.nike.cerberus.client.auth.aws;

import com.amazonaws.AmazonClientException;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.kms.AWSKMSClient;
import com.amazonaws.util.EC2MetadataUtils;
import com.google.gson.JsonSyntaxException;
import com.nike.vault.client.UrlResolver;
import com.nike.vault.client.VaultClientException;
import com.nike.vault.client.auth.TokenVaultCredentials;
import com.nike.vault.client.auth.VaultCredentialsProvider;
import com.nike.vault.client.model.VaultAuthResponse;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
Expand All @@ -45,6 +42,8 @@ public class InstanceRoleVaultCredentialsProvider extends BaseAwsCredentialsProv

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

public static final Pattern IAM_ARN_PATTERN = Pattern.compile("(arn\\:aws\\:iam\\:\\:)(?<accountId>[0-9].*)(\\:.*)");

/**
* Constructor to setup credentials provider using the specified
* implementation of {@link UrlResolver}
Expand All @@ -68,24 +67,13 @@ protected void authenticate() {
final Set<String> iamRoleSet = EC2MetadataUtils.getIAMSecurityCredentials().keySet();
final String accountId = lookupAccountId();

final AWSKMSClient kmsClient = new AWSKMSClient();
kmsClient.setRegion(Regions.getCurrentRegion());

for (final String iamRole : iamRoleSet) {
try {
final String encryptedAuthData = getEncryptedAuthData(accountId, iamRole);
final VaultAuthResponse decryptedToken = decryptToken(kmsClient, encryptedAuthData);
final DateTime expires = DateTime.now(DateTimeZone.UTC);
expires.plusSeconds(decryptedToken.getLeaseDuration() - paddingTimeInSeconds);

credentials = new TokenVaultCredentials(decryptedToken.getClientToken());
expireDateTime = expires;

getAndSetToken(accountId, iamRole);
return;
} catch (VaultClientException sce) {
LOGGER.warn("Unable to acquire Vault token for IAM role: " + iamRole, sce);
}

}
} catch (AmazonClientException ace) {
LOGGER.warn("Unexpected error communicating with AWS services.", ace);
Expand All @@ -95,4 +83,30 @@ protected void authenticate() {

throw new VaultClientException("Unable to acquire token with EC2 instance role.");
}

/**
* Parses and returns the AWS account ID from the instance profile ARN.
*
* @return AWS account ID
*/
protected String lookupAccountId() {
final EC2MetadataUtils.IAMInfo iamInfo = EC2MetadataUtils.getIAMInstanceProfileInfo();

if (iamInfo == null) {
final String errorMessage = "No IAM Instance Profile assigned to running instance.";
LOGGER.error(errorMessage);
throw new VaultClientException(errorMessage);
}

final Matcher matcher = IAM_ARN_PATTERN.matcher(iamInfo.instanceProfileArn);

if (matcher.matches()) {
final String accountId = matcher.group("accountId");
if (StringUtils.isNotBlank(accountId)) {
return accountId;
}
}

throw new VaultClientException("Unable to obtain AWS account ID from instance profile ARN.");
}
}
Loading

0 comments on commit 4f51ce3

Please sign in to comment.