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

Support use of IRSA for repository-s3 plugin credentials #3475

Merged
merged 3 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 plugins/repository-s3/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ versions << [
dependencies {
api "com.amazonaws:aws-java-sdk-s3:${versions.aws}"
api "com.amazonaws:aws-java-sdk-core:${versions.aws}"
api "com.amazonaws:aws-java-sdk-sts:${versions.aws}"
api "com.amazonaws:jmespath-java:${versions.aws}"
api "org.apache.httpcomponents:httpclient:${versions.httpclient}"
api "org.apache.httpcomponents:httpcore:${versions.httpcore}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
724bd22c0ff41c496469e18f9bea12bdfb2f7540
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ final class S3ClientSettings {
/** Placeholder client name for normalizing client settings in the repository settings. */
private static final String PLACEHOLDER_CLIENT = "placeholder";

// Properties to support using IAM Roles for Service Accounts (IRSA)

/** The identity token file for connecting to s3. */
static final Setting.AffixSetting<String> IDENTITY_TOKEN_FILE_SETTING = Setting.affixKeySetting(
PREFIX,
"identity_token_file",
key -> SecureSetting.simpleString(key, Property.NodeScope)
);

/** The role ARN (Amazon Resource Name) for connecting to s3. */
static final Setting.AffixSetting<SecureString> ROLE_ARN_SETTING = Setting.affixKeySetting(
PREFIX,
"role_arn",
key -> SecureSetting.secureString(key, null)
);

/** The role session name for connecting to s3. */
static final Setting.AffixSetting<SecureString> ROLE_SESSION_NAME_SETTING = Setting.affixKeySetting(
PREFIX,
"role_session_name",
key -> SecureSetting.secureString(key, null)
);

/** The access key (ie login id) for connecting to s3. */
static final Setting.AffixSetting<SecureString> ACCESS_KEY_SETTING = Setting.affixKeySetting(
PREFIX,
Expand Down Expand Up @@ -189,6 +212,9 @@ final class S3ClientSettings {
/** Credentials to authenticate with s3. */
final S3BasicCredentials credentials;

/** Credentials to authenticate with s3 using IAM Roles for Service Accounts (IRSA). */
final IrsaCredentials irsaCredentials;

/** The s3 endpoint the client should talk to, or empty string to use the default. */
final String endpoint;

Expand Down Expand Up @@ -221,6 +247,7 @@ final class S3ClientSettings {

private S3ClientSettings(
S3BasicCredentials credentials,
IrsaCredentials irsaCredentials,
String endpoint,
Protocol protocol,
int readTimeoutMillis,
Expand All @@ -233,6 +260,7 @@ private S3ClientSettings(
ProxySettings proxySettings
) {
this.credentials = credentials;
this.irsaCredentials = irsaCredentials;
this.endpoint = endpoint;
this.protocol = protocol;
this.readTimeoutMillis = readTimeoutMillis;
Expand Down Expand Up @@ -301,6 +329,7 @@ S3ClientSettings refine(Settings repositorySettings) {
validateInetAddressFor(newProxyHost);
return new S3ClientSettings(
newCredentials,
irsaCredentials,
newEndpoint,
newProtocol,
newReadTimeoutMillis,
Expand Down Expand Up @@ -396,12 +425,27 @@ private static S3BasicCredentials loadCredentials(Settings settings, String clie
}
}

private static IrsaCredentials loadIrsaCredentials(Settings settings, String clientName) {
String identityTokenFile = getConfigValue(settings, clientName, IDENTITY_TOKEN_FILE_SETTING);
try (
SecureString roleArn = getConfigValue(settings, clientName, ROLE_ARN_SETTING);
SecureString roleSessionName = getConfigValue(settings, clientName, ROLE_SESSION_NAME_SETTING)
) {
if (identityTokenFile.length() != 0 || roleArn.length() != 0 || roleSessionName.length() != 0) {
return new IrsaCredentials(identityTokenFile.toString(), roleArn.toString(), roleSessionName.toString());
}

return null;
}
}

// pkg private for tests
/** Parse settings for a single client. */
static S3ClientSettings getClientSettings(final Settings settings, final String clientName) {
final Protocol awsProtocol = getConfigValue(settings, clientName, PROTOCOL_SETTING);
return new S3ClientSettings(
S3ClientSettings.loadCredentials(settings, clientName),
S3ClientSettings.loadIrsaCredentials(settings, clientName),
getConfigValue(settings, clientName, ENDPOINT_SETTING),
awsProtocol,
Math.toIntExact(getConfigValue(settings, clientName, READ_TIMEOUT_SETTING).millis()),
Expand Down Expand Up @@ -482,7 +526,8 @@ public boolean equals(final Object o) {
&& proxySettings.equals(that.proxySettings)
&& Objects.equals(disableChunkedEncoding, that.disableChunkedEncoding)
&& Objects.equals(region, that.region)
&& Objects.equals(signerOverride, that.signerOverride);
&& Objects.equals(signerOverride, that.signerOverride)
&& Objects.equals(irsaCredentials, that.irsaCredentials);
}

@Override
Expand Down Expand Up @@ -512,4 +557,51 @@ private static <T> T getRepoSettingOrDefault(Setting.AffixSetting<T> setting, Se
}
return defaultValue;
}

/**
* Class to store IAM Roles for Service Accounts (IRSA) credentials
* See please: https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
*/
static class IrsaCredentials {
private final String identityTokenFile;
private final String roleArn;
private final String roleSessionName;

IrsaCredentials(String identityTokenFile, String roleArn, String roleSessionName) {
this.identityTokenFile = Strings.isNullOrEmpty(identityTokenFile) ? null : identityTokenFile;
this.roleArn = Strings.isNullOrEmpty(roleArn) ? null : roleArn;
this.roleSessionName = Strings.isNullOrEmpty(roleSessionName) ? "s3-sdk-java-" + System.currentTimeMillis() : roleSessionName;
}

public String getIdentityTokenFile() {
return identityTokenFile;
}

public String getRoleArn() {
return roleArn;
}

public String getRoleSessionName() {
return roleSessionName;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final IrsaCredentials that = (IrsaCredentials) o;
return Objects.equals(identityTokenFile, that.identityTokenFile)
&& Objects.equals(roleArn, that.roleArn)
&& Objects.equals(roleSessionName, that.roleSessionName);
}

@Override
public int hashCode() {
return Objects.hash(identityTokenFile, roleArn, roleSessionName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ public List<Setting<?>> getSettings() {
S3Repository.ACCESS_KEY_SETTING,
S3Repository.SECRET_KEY_SETTING,
S3ClientSettings.SIGNER_OVERRIDE,
S3ClientSettings.REGION
S3ClientSettings.REGION,
S3ClientSettings.ROLE_ARN_SETTING,
S3ClientSettings.IDENTITY_TOKEN_FILE_SETTING,
S3ClientSettings.ROLE_SESSION_NAME_SETTING
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSSessionCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper;
import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider;
import com.amazonaws.auth.STSAssumeRoleWithWebIdentitySessionCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.http.IdleConnectionReaper;
import com.amazonaws.http.SystemPropertyTlsKeyManagersProvider;
Expand All @@ -45,16 +48,20 @@
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.internal.Constants;
import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;

import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.protocol.HttpContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.opensearch.cluster.metadata.RepositoryMetadata;
import org.opensearch.common.Nullable;
import org.opensearch.common.Strings;
import org.opensearch.common.collect.MapBuilder;
import org.opensearch.common.settings.Settings;
import org.opensearch.repositories.s3.S3ClientSettings.IrsaCredentials;

import javax.net.ssl.SSLContext;
import java.io.Closeable;
Expand All @@ -66,13 +73,19 @@
import java.net.Socket;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static com.amazonaws.SDKGlobalConfiguration.AWS_ROLE_ARN_ENV_VAR;
import static com.amazonaws.SDKGlobalConfiguration.AWS_ROLE_SESSION_NAME_ENV_VAR;
import static com.amazonaws.SDKGlobalConfiguration.AWS_WEB_IDENTITY_ENV_VAR;
import static java.util.Collections.emptyMap;

class S3Service implements Closeable {
private static final Logger logger = LogManager.getLogger(S3Service.class);

private volatile Map<S3ClientSettings, AmazonS3Reference> clientsCache = emptyMap();
private Set<Closeable> credentialsCache = ConcurrentHashMap.newKeySet();

/**
* Client settings calculated from static configuration and settings in the keystore.
Expand Down Expand Up @@ -165,7 +178,13 @@ S3ClientSettings settings(RepositoryMetadata repositoryMetadata) {
// proxy for testing
AmazonS3 buildClient(final S3ClientSettings clientSettings) {
final AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();
builder.withCredentials(buildCredentials(logger, clientSettings));

final AWSCredentialsProvider credentials = buildCredentials(logger, clientSettings);
if (credentials instanceof Closeable) {
credentialsCache.add((Closeable) credentials);
}

builder.withCredentials(credentials);
builder.withClientConfiguration(buildConfiguration(clientSettings));

String endpoint = Strings.hasLength(clientSettings.endpoint) ? clientSettings.endpoint : Constants.S3_HOSTNAME;
Expand Down Expand Up @@ -259,23 +278,91 @@ public Socket createSocket(final HttpContext ctx) throws IOException {
// pkg private for tests
static AWSCredentialsProvider buildCredentials(Logger logger, S3ClientSettings clientSettings) {
final S3BasicCredentials credentials = clientSettings.credentials;
reta marked this conversation as resolved.
Show resolved Hide resolved
if (credentials == null) {
logger.debug("Using instance profile credentials");
return new PrivilegedInstanceProfileCredentialsProvider();
} else {
final IrsaCredentials irsaCredentials = buildFromEnviroment(clientSettings.irsaCredentials);

// If IAM Roles for Service Accounts (IRSA) credentials are configured, start with them first
if (irsaCredentials != null) {
logger.debug("Using IRSA credentials");

AWSSecurityTokenService securityTokenService = null;
final String region = Strings.hasLength(clientSettings.region) ? clientSettings.region : null;
if (region != null || credentials != null) {
securityTokenService = SocketAccess.doPrivileged(
() -> AWSSecurityTokenServiceClientBuilder.standard()
.withCredentials((credentials != null) ? new AWSStaticCredentialsProvider(credentials) : null)
.withRegion(region)
.build()
);
}

if (irsaCredentials.getIdentityTokenFile() == null) {
return new PrivilegedSTSAssumeRoleSessionCredentialsProvider<>(
securityTokenService,
new STSAssumeRoleSessionCredentialsProvider.Builder(irsaCredentials.getRoleArn(), irsaCredentials.getRoleSessionName())
.withStsClient(securityTokenService)
.build()
);
} else {
return new PrivilegedSTSAssumeRoleSessionCredentialsProvider<>(
securityTokenService,
new STSAssumeRoleWithWebIdentitySessionCredentialsProvider.Builder(
irsaCredentials.getRoleArn(),
irsaCredentials.getRoleSessionName(),
irsaCredentials.getIdentityTokenFile()
).withStsClient(securityTokenService).build()
);
}
} else if (credentials != null) {
logger.debug("Using basic key/secret credentials");
return new AWSStaticCredentialsProvider(credentials);
} else {
logger.debug("Using instance profile credentials");
return new PrivilegedInstanceProfileCredentialsProvider();
}
}

private static IrsaCredentials buildFromEnviroment(IrsaCredentials defaults) {
if (defaults == null) {
return null;
}

String webIdentityTokenFile = defaults.getIdentityTokenFile();
if (webIdentityTokenFile == null) {
webIdentityTokenFile = System.getenv(AWS_WEB_IDENTITY_ENV_VAR);
}

String roleArn = defaults.getRoleArn();
if (roleArn == null) {
roleArn = System.getenv(AWS_ROLE_ARN_ENV_VAR);
}

String roleSessionName = defaults.getRoleSessionName();
if (roleSessionName == null) {
roleSessionName = System.getenv(AWS_ROLE_SESSION_NAME_ENV_VAR);
}

return new IrsaCredentials(webIdentityTokenFile, roleArn, roleSessionName);
}

private synchronized void releaseCachedClients() {
// the clients will shutdown when they will not be used anymore
for (final AmazonS3Reference clientReference : clientsCache.values()) {
clientReference.decRef();
}

for (final Closeable closeable : credentialsCache) {
try {
closeable.close();
reta marked this conversation as resolved.
Show resolved Hide resolved
} catch (IOException e) {
/* Ignoring */
}
}

// clear previously cached clients, they will be build lazily
clientsCache = emptyMap();
derivedClientSettings = emptyMap();
credentialsCache.clear();

// shutdown IdleConnectionReaper background thread
// it will be restarted on new client usage
IdleConnectionReaper.shutdown();
Expand All @@ -300,6 +387,43 @@ public void refresh() {
}
}

static class PrivilegedSTSAssumeRoleSessionCredentialsProvider<P extends AWSSessionCredentialsProvider & Closeable>
implements
AWSCredentialsProvider,
Closeable {
private final P credentials;
private final AWSSecurityTokenService securityTokenService;

private PrivilegedSTSAssumeRoleSessionCredentialsProvider(
@Nullable final AWSSecurityTokenService securityTokenService,
final P credentials
) {
this.securityTokenService = securityTokenService;
this.credentials = credentials;
}

@Override
public AWSCredentials getCredentials() {
return SocketAccess.doPrivileged(credentials::getCredentials);
}

@Override
public void refresh() {
SocketAccess.doPrivilegedVoid(credentials::refresh);
}

@Override
public void close() throws IOException {
SocketAccess.doPrivilegedIOException(() -> {
credentials.close();
if (securityTokenService != null) {
securityTokenService.shutdown();
}
return null;
});
};
}

@Override
public void close() {
releaseCachedClients();
Expand Down
Loading