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 22, 2021
1 parent 0af0925 commit 6ff66d1
Show file tree
Hide file tree
Showing 15 changed files with 739 additions and 26 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
* Fix #548: Define property for skipping cluster autodetect/offline mode

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jkube.kit.common.KitLogger;

import java.nio.charset.StandardCharsets;
import java.util.Map;
Expand All @@ -35,34 +37,43 @@
@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;
authEncoded = createAuthEncoded();
this.identityToken = identityToken;
}

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

private String createAuthEncoded() {
public String toHeaderValue(KitLogger logger) {
JsonObject ret = new JsonObject();
putNonNull(ret, "username", username);
putNonNull(ret, "password", password);
putNonNull(ret, "email", email);
putNonNull(ret, "auth", auth);
if(StringUtils.isNotBlank(identityToken)) {
putNonNull(ret, "identityToken", identityToken);
if (StringUtils.isNotBlank(username)) {
logger.warn("Using identityToken, found username not blank : " + username);
}
} else {
putNonNull(ret, "username", username);
putNonNull(ret, "password", password);
putNonNull(ret, "email", email);
putNonNull(ret, "auth", auth);
}

return encodeBase64ChunkedURLSafeString(ret.toString().getBytes(StandardCharsets.UTF_8));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public void testFromPluginConfigurationFailed() {
}

private void verifyAuthConfig(AuthConfig config, String username, String password, String email) {
JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue().getBytes())), JsonObject.class);
JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue(log).getBytes())), JsonObject.class);
assertEquals(username, params.get("username").getAsString());
assertEquals(password, params.get("password").getAsString());
if (email != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private void createOpenShiftConfig(File homeDir, String testConfig) {
}

private void verifyAuthConfig(AuthConfig config, String username, String password, String email) {
JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue().getBytes())), JsonObject.class);
JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue(log).getBytes())), JsonObject.class);
assertEquals(username,params.get("username").getAsString());
assertEquals(password,params.get("password").getAsString());
if (email != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private void checkException(String key) throws IOException {
}

private void verifyAuthConfig(AuthConfig config, String username, String password, String email) {
JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue().getBytes())), JsonObject.class);
JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue(log).getBytes())), JsonObject.class);
assertEquals(username,params.get("username").getAsString());
assertEquals(password,params.get("password").getAsString());
if (email != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ private Map<String, String> createAuthHeader(AuthConfig authConfig) {
if (authConfig == null) {
authConfig = AuthConfig.EMPTY_AUTH_CONFIG;
}
return Collections.singletonMap("X-Registry-Auth", authConfig.toHeaderValue());
return Collections.singletonMap("X-Registry-Auth", authConfig.toHeaderValue(log));
}

private boolean isRetryableErrorCode(int errorCode) {
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 final 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,108 @@ 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 AuthConfig.builder()
.username(user)
.password(password)
.email("none")
.auth(token)
.build();
}
}
}

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;
}

try {
return new URI(awsSdkHelper.getEcsMetadataEndpoint() + 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://www.eclipse.org/jkube/docs/kubernetes-maven-plugin#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 AuthConfig.builder()
.username(accessKeyId)
.password(secretAccessKey)
.email("none")
.auth(awsSdkHelper.getAwsSessionTokenEnvVar())
.build();
}

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

private static Map<String, String> getAuthConfigMapToCheck(LookupMode lookupMode, Map<?, ?> authConfigMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class CredentialHelperClient {

static final String SECRET_KEY = "Secret";
static final String USERNAME_KEY = "Username";
static final String TOKEN_USERNAME = "<token>";
private final String credentialHelperName;
private final KitLogger log;

Expand Down Expand Up @@ -61,12 +62,16 @@ public AuthConfig getAuthConfig(String registryToLookup) throws IOException {
}
}

private AuthConfig toAuthConfig(JsonObject credential){
AuthConfig toAuthConfig(JsonObject credential){
if (credential == null) {
return null;
}
String password = credential.get(CredentialHelperClient.SECRET_KEY).getAsString();
String userKey = credential.get(CredentialHelperClient.USERNAME_KEY).getAsString();
if (TOKEN_USERNAME.equals(userKey)) {
// If userKey is <token>, the password is actually a token
return new AuthConfig(null, null, null, null, password);
}
return new AuthConfig(userKey,password, null,null);
}

Expand Down
Loading

0 comments on commit 6ff66d1

Please sign in to comment.