Skip to content

Commit

Permalink
Implemented authentication via EC2 instance role for ECR
Browse files Browse the repository at this point in the history
Signed-off-by: Thorsten Meinl <thorsten.meinl@knime.com>
  • Loading branch information
thorsten-meinl-knime committed Apr 6, 2019
1 parent 01f0f72 commit ebed8a6
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 15 deletions.
1 change: 1 addition & 0 deletions doc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/main/asciidoc/inc/_authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<auth>` 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.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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.
*
Expand Down
97 changes: 92 additions & 5 deletions src/main/java/io/fabric8/maven/docker/util/AuthConfigFactory.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
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;
import java.util.Properties;
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;
Expand All @@ -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
*
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit ebed8a6

Please sign in to comment.