Skip to content

Commit

Permalink
Fetch AWS Credentials from AWS SDK/Environment variables
Browse files Browse the repository at this point in the history
Add support in AuthConfigFactory to retrieve credentials from
different mechanisms for AWS.

Port of fabric8io/docker-maven-plugin#1311
Port of fabric8io/docker-maven-plugin#1310

Related to #702

Signed-off-by: Rohan Kumar <rohaan@redhat.com>
  • Loading branch information
rohanKanojia committed Jun 18, 2021
1 parent 32be156 commit 91cf841
Show file tree
Hide file tree
Showing 8 changed files with 599 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Usage:
* Fix #705: JIB assembly works on Windows
* Fix #714: feat: Helm support for Golang expressions
* Port fabric8io/docker-maven-plugin#1318: Update ECR autorization token URL
* Port fabric8io/docker-maven-plugin#1311: Use AWS SDK to fetch AWS credentials
* Fix #710: Support DockerImage as output for Openshift builds

### 1.3.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import lombok.Getter;
import org.apache.commons.codec.binary.Base64;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;
Expand All @@ -35,35 +36,50 @@
@EqualsAndHashCode
public class AuthConfig {

public static final AuthConfig EMPTY_AUTH_CONFIG = new AuthConfig("", "", "", "");
public static final AuthConfig EMPTY_AUTH_CONFIG = new AuthConfig("", "", "", "", "");

private final String username;
private final String password;
private final String email;
private final String auth;
private final String identityToken;

private String authEncoded;

@Builder
public AuthConfig(String username, String password, String email, String auth) {
public AuthConfig(String username, String password, String email, String auth, String identityToken) {
this.username = username;
this.password = password;
this.email = email;
this.auth = auth;
this.identityToken = identityToken;
authEncoded = createAuthEncoded();
}

public AuthConfig(String username, String password, String email, String auth) {
this(username, password, email, auth, null);
}

public String toHeaderValue() {
return authEncoded;
}

private String createAuthEncoded() {
JsonObject ret = new JsonObject();
putNonNull(ret, "username", username);
putNonNull(ret, "password", password);
putNonNull(ret, "email", email);
putNonNull(ret, "auth", auth);
return encodeBase64ChunkedURLSafeString(ret.toString().getBytes(StandardCharsets.UTF_8));
if(identityToken != null) {
putNonNull(ret, "identityToken", identityToken);
} else {
putNonNull(ret, "username", username);
putNonNull(ret, "password", password);
putNonNull(ret, "email", email);
putNonNull(ret, "auth", auth);
}

try {
return encodeBase64ChunkedURLSafeString(ret.toString().getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
return encodeBase64ChunkedURLSafeString(ret.toString().getBytes());
}
}

public static AuthConfig fromMap(Map<String, String> params) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jkube.kit.build.service.docker.auth.ecr.AwsSdkAuthConfigFactory;
import org.eclipse.jkube.kit.build.service.docker.auth.ecr.AwsSdkHelper;
import org.eclipse.jkube.kit.common.RegistryServerConfiguration;
import org.eclipse.jkube.kit.build.api.helper.DockerFileUtil;
import org.eclipse.jkube.kit.build.api.auth.AuthConfig;
Expand All @@ -35,11 +37,12 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -63,12 +66,18 @@ public class AuthConfigFactory {
private static final String DOCKER_LOGIN_DEFAULT_REGISTRY = "https://index.docker.io/v1/";

private final KitLogger log;
private AwsSdkHelper awsSdkHelper;
private static final String[] DEFAULT_REGISTRIES = new String[]{
"docker.io", "index.docker.io", "registry.hub.docker.com"
};

public AuthConfigFactory(KitLogger log) {
this(log, new AwsSdkHelper());
}

AuthConfigFactory(KitLogger log, AwsSdkHelper awsSdkHelper) {
this.log = log;
this.awsSdkHelper = awsSdkHelper;
}

/**
Expand Down Expand Up @@ -111,7 +120,7 @@ public AuthConfigFactory(KitLogger log) {
public AuthConfig createAuthConfig(boolean isPush, boolean skipExtendedAuth, Map authConfig, List<RegistryServerConfiguration> settings, String user, String registry, UnaryOperator<String> passwordDecryptionMethod)
throws IOException {

AuthConfig ret = createStandardAuthConfig(isPush, authConfig, settings, user, registry, passwordDecryptionMethod, log);
AuthConfig ret = createStandardAuthConfig(isPush, authConfig, settings, user, registry, passwordDecryptionMethod, log, awsSdkHelper);
if (ret != null) {
if (registry == null || skipExtendedAuth) {
return ret;
Expand Down Expand Up @@ -185,7 +194,7 @@ private AuthConfig extendedAuthentication(AuthConfig standardAuthConfig, String
*
* @throws IOException any exception in case of fetching authConfig
*/
private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfigMap, List<RegistryServerConfiguration> settings, String user, String registry, UnaryOperator<String> passwordDecryptionMethod, KitLogger log)
private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfigMap, List<RegistryServerConfiguration> settings, String user, String registry, UnaryOperator<String> passwordDecryptionMethod, KitLogger log, AwsSdkHelper awsSdkHelper)
throws IOException {
AuthConfig ret;

Expand Down Expand Up @@ -225,6 +234,18 @@ private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfi

// check EC2 instance role if registry is ECR
if (EcrExtendedAuth.isAwsRegistry(registry)) {
ret = getAuthConfigViaAwsSdk(awsSdkHelper, log);
if (ret != null) {
log.debug("AuthConfig: AWS credentials from AWS SDK");
return ret;
}

ret = getAuthConfigFromAwsEnvironmentVariables(awsSdkHelper, log);
if (ret != null) {
log.debug("AuthConfig: AWS credentials from ENV variables");
return ret;
}

try {
ret = getAuthConfigFromEC2InstanceRole(log);
} catch (ConnectTimeoutException ex) {
Expand All @@ -238,6 +259,18 @@ private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfi
log.debug("AuthConfig: credentials from EC2 instance role");
return ret;
}
try {
ret = getAuthConfigFromTaskRole(awsSdkHelper, log);
} catch (ConnectTimeoutException ex) {
log.debug("Connection timeout while retrieving ECS meta-data, likely not an ECS instance (%s)",
ex.getMessage());
} catch (IOException ex) {
log.warn("Error while retrieving ECS Task role credentials: %s", ex.getMessage());
}
if (ret != null) {
log.debug("AuthConfig: credentials from ECS Task role");
return ret;
}
}

// No authentication found
Expand Down Expand Up @@ -433,6 +466,103 @@ private static JsonObject getCredentialsNode(JsonObject auths,String registryToL
return null;
}

// if the local credentials don't contain user and password & is not a EC2 instance,
// use ECS|Fargate Task instance role credentials
private static AuthConfig getAuthConfigFromTaskRole(AwsSdkHelper awsSdkHelper, KitLogger log) throws IOException {
log.debug("No user and password set for ECR, checking ECS Task role");
URI uri = getMetadataEndpointForCredentials(awsSdkHelper, log);
if (uri == null) {
return null;
}
// get temporary credentials
log.debug("Getting temporary security credentials from: %s", uri);
try (CloseableHttpClient client = HttpClients.custom().useSystemProperties().build()) {
RequestConfig conf =
RequestConfig.custom().setConnectionRequestTimeout(1000).setConnectTimeout(1000)
.setSocketTimeout(1000).build();
HttpGet request = new HttpGet(uri);
request.setConfig(conf);
return readAwsCredentials(client, request, log);
}
}


private static AuthConfig readAwsCredentials(CloseableHttpClient client, HttpGet request, KitLogger log) throws IOException {
try (CloseableHttpResponse response = client.execute(request)) {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
log.debug("No security credential found, return code was %d",
response.getStatusLine().getStatusCode());
// no instance role found
return null;
}

// read instance role
try (Reader r = new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)) {
JsonObject securityCredentials = new Gson().fromJson(r, JsonObject.class);

String user = securityCredentials.getAsJsonPrimitive("AccessKeyId").getAsString();
String password = securityCredentials.getAsJsonPrimitive("SecretAccessKey").getAsString();
String token = securityCredentials.getAsJsonPrimitive("Token").getAsString();

log.debug("Received temporary access key %s...", user.substring(0, 8));
return new AuthConfig(user, password, "none", token);
}
}
}

private static URI getMetadataEndpointForCredentials(AwsSdkHelper awsSdkHelper, KitLogger log) {
// get ECS task role - if available
String awsContainerCredentialsUri = awsSdkHelper.getAwsContainerCredentialsRelativeUri();
if (awsContainerCredentialsUri == null) {
log.debug("System environment not set for variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, no task role found");
return null;
}
if (awsContainerCredentialsUri.charAt(0) != '/') {
awsContainerCredentialsUri = "/" + awsContainerCredentialsUri;
}

String ecsMetadataEndpoint = awsSdkHelper.getEcsMetadataEndpoint();
if (ecsMetadataEndpoint == null) {
ecsMetadataEndpoint = "http://169.254.170.2";
}

try {
return new URI(ecsMetadataEndpoint + awsContainerCredentialsUri);
} catch (URISyntaxException e) {
log.warn("Failed to construct path to ECS metadata endpoint for credentials", e);
return null;
}
}

private static AuthConfig getAuthConfigViaAwsSdk(AwsSdkHelper awsSdkHelper, KitLogger log) {
boolean credProviderPresent = awsSdkHelper.isDefaultAWSCredentialsProviderChainPresentInClassPath();
if (!credProviderPresent) {
log.info("It appears that you're using AWS ECR." +
" Consider integrating the AWS SDK in order to make use of common AWS authentication mechanisms," +
" see https://dmp.fabric8.io/#extended-authentication");
return null;
}
return new AwsSdkAuthConfigFactory(log, awsSdkHelper).createAuthConfig();
}

/**
* Try using the AWS credentials provided via ENV variables.
* See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
*/
private static AuthConfig getAuthConfigFromAwsEnvironmentVariables(AwsSdkHelper awsSdkHelper, KitLogger log) {
String accessKeyId = awsSdkHelper.getAwsAccessKeyIdEnvVar();
if (accessKeyId == null) {
log.debug("System environment not set for variable AWS_ACCESS_KEY_ID, no AWS credentials found");
return null;
}
String secretAccessKey = awsSdkHelper.getAwsSecretAccessKeyEnvVar();
if (secretAccessKey == null) {
log.warn("System environment set for variable AWS_ACCESS_KEY_ID, but NOT for variable AWS_SECRET_ACCESS_KEY!");
return null;
}
return new AuthConfig(accessKeyId, secretAccessKey, "none", awsSdkHelper.getAwsSessionTokenEnvVar());
}

// =======================================================================================================

private static Map<String, String> getAuthConfigMapToCheck(LookupMode lookupMode, Map<?, ?> authConfigMap) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.eclipse.jkube.kit.build.service.docker.auth.ecr;

import org.eclipse.jkube.kit.build.api.auth.AuthConfig;
import org.eclipse.jkube.kit.common.KitLogger;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import static java.nio.charset.StandardCharsets.UTF_8;

public class AwsSdkAuthConfigFactory {

private final KitLogger log;
private AwsSdkHelper awsSdkHelper;

public AwsSdkAuthConfigFactory(KitLogger log, AwsSdkHelper awsSdkHelper) {
this.log = log;
this.awsSdkHelper = awsSdkHelper;
}

public AuthConfig createAuthConfig() {
try {
Object credentials = awsSdkHelper.getCredentialsFromDefaultAWSCredentialsProviderChain();
if (credentials == null) {
return null;
}

return new AuthConfig(
awsSdkHelper.getAWSAccessKeyIdFromCredentials(credentials),
awsSdkHelper.getAwsSecretKeyFromCredentials(credentials),
"none",
awsSdkHelper.getSessionTokenFromCrendentials(credentials)
);
} catch (Exception t) {
String issueTitle = null;
try {
issueTitle = URLEncoder.encode("Failed calling AWS SDK: " + t.getMessage(), UTF_8.name());
} catch (UnsupportedEncodingException ignore) {
}
log.warn("Failed to fetch AWS credentials: %s", t.getMessage());
if (t.getCause() != null) {
log.warn("Caused by: %s", t.getCause().getMessage());
}
log.warn("Please report a bug at https://github.com/eclipse/jkube/issues/new?%s",
issueTitle == null ? "" : "title=?" + issueTitle);
log.warn("%s", t);
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.eclipse.jkube.kit.build.service.docker.auth.ecr;

import java.lang.reflect.InvocationTargetException;

public class AwsSdkHelper {
public static final String ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID";
public static final String SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY";
public static final String SESSION_TOKEN = "AWS_SESSION_TOKEN";
public static final String CONTAINER_CREDENTIALS_RELATIVE_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
public static final String METADATA_ENDPOINT = "ECS_METADATA_ENDPOINT";

public boolean isDefaultAWSCredentialsProviderChainPresentInClassPath() {
try {
Class.forName("com.amazonaws.auth.DefaultAWSCredentialsProviderChain");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}

public String getAwsAccessKeyIdEnvVar() {
return System.getenv(ACCESS_KEY_ID);
}

public String getAwsSecretAccessKeyEnvVar() {
return System.getenv(SECRET_ACCESS_KEY);
}

public String getAwsSessionTokenEnvVar() {
return System.getenv(SESSION_TOKEN);
}

public String getAwsContainerCredentialsRelativeUri() {
return System.getenv(CONTAINER_CREDENTIALS_RELATIVE_URI);
}

public String getEcsMetadataEndpoint() {
return System.getenv(METADATA_ENDPOINT);
}

public Object getCredentialsFromDefaultAWSCredentialsProviderChain() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> credentialsProviderChainClass = Class.forName("com.amazonaws.auth.DefaultAWSCredentialsProviderChain");
Object credentialsProviderChain = credentialsProviderChainClass.getDeclaredConstructor().newInstance();
return credentialsProviderChainClass.getMethod("getCredentials").invoke(credentialsProviderChain);
}

public String getSessionTokenFromCrendentials(Object credentials) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> sessionCredentialsClass = Class.forName("com.amazonaws.auth.AWSSessionCredentials");
return sessionCredentialsClass.isInstance(credentials)
? (String) sessionCredentialsClass.getMethod("getSessionToken").invoke(credentials) : null;
}

public String getAWSAccessKeyIdFromCredentials(Object credentials) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> credentialsClass = Class.forName("com.amazonaws.auth.AWSCredentials");
return (String) credentialsClass.getMethod("getAWSAccessKeyId").invoke(credentials);
}

public String getAwsSecretKeyFromCredentials(Object credentials) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> credentialsClass = Class.forName("com.amazonaws.auth.AWSCredentials");
return (String) credentialsClass.getMethod("getAWSSecretKey").invoke(credentials);
}
}
Loading

0 comments on commit 91cf841

Please sign in to comment.