Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch AWS Credentials from AWS SDK/Environment variables #730

Merged
merged 1 commit into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
manusa marked this conversation as resolved.
Show resolved Hide resolved
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