From ebed8a63d4315aba1dce18e88bce3af4c55168e3 Mon Sep 17 00:00:00 2001 From: Thorsten Meinl Date: Tue, 2 Apr 2019 22:07:57 +0200 Subject: [PATCH] Implemented authentication via EC2 instance role for ECR Signed-off-by: Thorsten Meinl --- doc/changelog.md | 1 + src/main/asciidoc/inc/_authentication.adoc | 6 ++ .../docker/access/ecr/EcrExtendedAuth.java | 30 ++++-- .../maven/docker/util/AuthConfigFactory.java | 97 ++++++++++++++++++- 4 files changed, 119 insertions(+), 15 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 42dd127f9..ada8083bc 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -9,6 +9,7 @@ - Update to jnr-unixsocket 0.22 - Support docker SHELL setting for runCmds (#1157) - Added 'autoRemove' option for running containers (#1179) + - Added support for AWS EC2 instance roles when pushing to AWS ECR (#1186) * **0.28.0** (2018-12-13) - Update to JMockit 1.43 diff --git a/src/main/asciidoc/inc/_authentication.adoc b/src/main/asciidoc/inc/_authentication.adoc index afd219134..208f0a54e 100644 --- a/src/main/asciidoc/inc/_authentication.adoc +++ b/src/main/asciidoc/inc/_authentication.adoc @@ -184,3 +184,9 @@ You can use any IAM access key with the necessary permissions in any of the loca Use the IAM *Access key ID* as the username and the *Secret access key* as the password. In case you're using temporary security credentials provided by the AWS Security Token Service (AWS STS), you have to provide the *security token* as well. To do so, either specify the `docker.authToken` system property or provide an `` element alongside username & password in the `authConfig`. + +In case you are running on an EC2 instance that has an appropriate IAM role assigned +(e.g. a role that grants the AWS built-in policy _AmazonEC2ContainerRegistryPowerUser_) +authentication information doesn't need to be provided at all. Instead the instance +meta-data service is queried for temporary access credentials supplied by the +assigned role. diff --git a/src/main/java/io/fabric8/maven/docker/access/ecr/EcrExtendedAuth.java b/src/main/java/io/fabric8/maven/docker/access/ecr/EcrExtendedAuth.java index 03791aa95..92a0b6bf4 100644 --- a/src/main/java/io/fabric8/maven/docker/access/ecr/EcrExtendedAuth.java +++ b/src/main/java/io/fabric8/maven/docker/access/ecr/EcrExtendedAuth.java @@ -1,8 +1,12 @@ package io.fabric8.maven.docker.access.ecr; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; @@ -13,13 +17,9 @@ import org.apache.http.impl.client.HttpClients; import org.apache.maven.plugin.MojoExecutionException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import io.fabric8.maven.docker.access.AuthConfig; import io.fabric8.maven.docker.util.Logger; @@ -40,6 +40,16 @@ public class EcrExtendedAuth { private final String accountId; private final String region; + /** + * Is given the registry an ecr registry? + * + * @param registry the registry name + * @return true, if the registry matches the ecr pattern + */ + public static boolean isAwsRegistry(String registry) { + return (registry != null) && AWS_REGISTRY.matcher(registry).matches(); + } + /** * Initialize an extended authentication for ecr registry. * diff --git a/src/main/java/io/fabric8/maven/docker/util/AuthConfigFactory.java b/src/main/java/io/fabric8/maven/docker/util/AuthConfigFactory.java index 9fa4f1388..521a377e0 100644 --- a/src/main/java/io/fabric8/maven/docker/util/AuthConfigFactory.java +++ b/src/main/java/io/fabric8/maven/docker/util/AuthConfigFactory.java @@ -1,14 +1,14 @@ package io.fabric8.maven.docker.util; -import com.google.gson.Gson; -import com.google.gson.JsonObject; - import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,8 +16,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import io.fabric8.maven.docker.access.AuthConfig; -import io.fabric8.maven.docker.access.ecr.EcrExtendedAuth; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.settings.Server; @@ -28,6 +34,13 @@ import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; import org.yaml.snakeyaml.Yaml; +import com.google.common.net.UrlEscapers; +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import io.fabric8.maven.docker.access.AuthConfig; +import io.fabric8.maven.docker.access.ecr.EcrExtendedAuth; + /** * Factory for creating docker specific authentication configuration * @@ -217,13 +230,87 @@ private AuthConfig createStandardAuthConfig(boolean isPush, Map authConfigMap, S log.debug("AuthConfig: credentials from ~/.m2/setting.xml"); return ret; } + + // check EC2 instance role if registry is ECR + if (EcrExtendedAuth.isAwsRegistry(registry)) { + try { + ret = getAuthConfigFromEC2InstanceRole(); + } catch (ConnectTimeoutException ex) { + log.debug("Connection timeout while retrieving instance meta-data, likely not an EC2 instance (%s)", + ex.getMessage()); + } catch (IOException ex) { + // don't make that an error since it may fail if not run on an EC2 instance + log.warn("Error while retrieving EC2 instance credentials: %s", ex.getMessage()); + } + if (ret != null) { + log.debug("AuthConfig: credentials from EC2 instance role"); + return ret; + } + } // No authentication found return null; } // =================================================================================================== + + + // if the local credentials don't contain user and password, use EC2 instance + // role credentials + private AuthConfig getAuthConfigFromEC2InstanceRole() throws IOException { + log.debug("No user and password set for ECR, checking EC2 instance role"); + try (CloseableHttpClient client = HttpClients.custom().useSystemProperties().build()) { + // we can set very low timeouts because the request returns almost instantly on + // an EC2 instance + // on a non-EC2 instance we can fail early + RequestConfig conf = RequestConfig.custom().setConnectionRequestTimeout(1000).setConnectTimeout(1000) + .setSocketTimeout(1000).build(); + + // get instance role - if available + HttpGet request = new HttpGet("http://169.254.169.254/latest/meta-data/iam/security-credentials"); + request.setConfig(conf); + String instanceRole; + try (CloseableHttpResponse response = client.execute(request)) { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + // no instance role found + log.debug("No instance role found, return code was %d", response.getStatusLine().getStatusCode()); + return null; + } + + // read instance role + try (InputStream is = response.getEntity().getContent()) { + instanceRole = IOUtils.toString(is, StandardCharsets.UTF_8); + } + } + log.debug("Found instance role %s, getting temporary security credentials", instanceRole); + + // get temporary credentials + request = new HttpGet("http://169.254.169.254/latest/meta-data/iam/security-credentials/" + + UrlEscapers.urlPathSegmentEscaper().escape(instanceRole)); + request.setConfig(conf); + 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 AuthConfig getAuthConfigFromSystemProperties(LookupMode lookupMode) throws MojoExecutionException { Properties props = System.getProperties(); String userKey = lookupMode.asSysProperty(AUTH_USERNAME);