Skip to content

Commit 17d0155

Browse files
authored
S3 Repository: Add back repository level credentials (#24609)
Specifying s3 access and secret keys inside repository settings are not secure. However, until there is a way to dynamically update secure settings, this is the only way to dynamically add repositories with credentials that are not known at node startup time. This commit adds back `access_key` and `secret_key` s3 repository settings, but protects it with a required system property `allow_insecure_settings`.
1 parent 1155615 commit 17d0155

File tree

6 files changed

+149
-17
lines changed

6 files changed

+149
-17
lines changed

core/src/main/java/org/elasticsearch/common/settings/SecureSetting.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,25 @@
2424
import java.util.EnumSet;
2525
import java.util.Set;
2626

27+
import org.elasticsearch.common.Booleans;
2728
import org.elasticsearch.common.util.ArrayUtils;
2829

29-
3030
/**
3131
* A secure setting.
3232
*
3333
* This class allows access to settings from the Elasticsearch keystore.
3434
*/
3535
public abstract class SecureSetting<T> extends Setting<T> {
36+
37+
/** Determines whether legacy settings with sensitive values should be allowed. */
38+
private static final boolean ALLOW_INSECURE_SETTINGS = Booleans.parseBoolean(System.getProperty("es.allow_insecure_settings", "false"));
39+
3640
private static final Set<Property> ALLOWED_PROPERTIES = EnumSet.of(Property.Deprecated, Property.Shared);
3741

3842
private static final Property[] FIXED_PROPERTIES = {
3943
Property.NodeScope
4044
};
4145

42-
private static final Property[] LEGACY_PROPERTIES = {
43-
Property.NodeScope, Property.Deprecated, Property.Filtered
44-
};
45-
4646
private SecureSetting(String key, Property... properties) {
4747
super(key, (String)null, null, ArrayUtils.concat(properties, FIXED_PROPERTIES, Property.class));
4848
assert assertAllowedProperties(properties);
@@ -133,6 +133,23 @@ SecureString getFallback(Settings settings) {
133133
};
134134
}
135135

136+
/**
137+
* A setting which contains a sensitive string, but which for legacy reasons must be found outside secure settings.
138+
* @see #secureString(String, Setting, Property...)
139+
*/
140+
public static Setting<SecureString> insecureString(String name) {
141+
return new Setting<SecureString>(name, "", SecureString::new, Property.Deprecated, Property.Filtered, Property.NodeScope) {
142+
@Override
143+
public SecureString get(Settings settings) {
144+
if (ALLOW_INSECURE_SETTINGS == false && exists(settings)) {
145+
throw new IllegalArgumentException("Setting [" + name + "] is insecure, " +
146+
"but property [allow_insecure_settings] is not set");
147+
}
148+
return super.get(settings);
149+
}
150+
};
151+
}
152+
136153
/**
137154
* A setting which contains a file. Reading the setting opens an input stream to the file.
138155
*

plugins/repository-s3/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ bundlePlugin {
5454
}
5555
}
5656

57+
additionalTest('testRepositoryCreds'){
58+
include '**/RepositorySettingsCredentialsTests.class'
59+
systemProperty 'es.allow_insecure_settings', 'true'
60+
}
61+
62+
test {
63+
// these are tested explicitly in separate test tasks
64+
exclude '**/*CredentialsTests.class'
65+
}
66+
5767
integTestCluster {
5868
keystoreSetting 's3.client.default.access_key', 'myaccesskey'
5969
keystoreSetting 's3.client.default.secret_key', 'mysecretkey'

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/InternalAwsS3Service.java

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.amazonaws.ClientConfiguration;
2727
import com.amazonaws.auth.AWSCredentials;
2828
import com.amazonaws.auth.AWSCredentialsProvider;
29+
import com.amazonaws.auth.BasicAWSCredentials;
2930
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
3031
import com.amazonaws.http.IdleConnectionReaper;
3132
import com.amazonaws.internal.StaticCredentialsProvider;
@@ -35,6 +36,8 @@
3536
import org.elasticsearch.ElasticsearchException;
3637
import org.elasticsearch.common.Strings;
3738
import org.elasticsearch.common.component.AbstractLifecycleComponent;
39+
import org.elasticsearch.common.logging.DeprecationLogger;
40+
import org.elasticsearch.common.settings.SecureString;
3841
import org.elasticsearch.common.settings.Setting;
3942
import org.elasticsearch.common.settings.Settings;
4043

@@ -69,7 +72,7 @@ public synchronized AmazonS3 client(Settings repositorySettings) {
6972

7073
logger.debug("creating S3 client with client_name [{}], endpoint [{}]", clientName, clientSettings.endpoint);
7174

72-
AWSCredentialsProvider credentials = buildCredentials(logger, clientSettings);
75+
AWSCredentialsProvider credentials = buildCredentials(logger, deprecationLogger, clientSettings, repositorySettings);
7376
ClientConfiguration configuration = buildConfiguration(clientSettings, repositorySettings);
7477

7578
client = new AmazonS3Client(credentials, configuration);
@@ -106,14 +109,42 @@ static ClientConfiguration buildConfiguration(S3ClientSettings clientSettings, S
106109
}
107110

108111
// pkg private for tests
109-
static AWSCredentialsProvider buildCredentials(Logger logger, S3ClientSettings clientSettings) {
110-
if (clientSettings.credentials == null) {
112+
static AWSCredentialsProvider buildCredentials(Logger logger, DeprecationLogger deprecationLogger,
113+
S3ClientSettings clientSettings, Settings repositorySettings) {
114+
115+
116+
BasicAWSCredentials credentials = clientSettings.credentials;
117+
if (S3Repository.ACCESS_KEY_SETTING.exists(repositorySettings)) {
118+
if (S3Repository.SECRET_KEY_SETTING.exists(repositorySettings) == false) {
119+
throw new IllegalArgumentException("Repository setting [" + S3Repository.ACCESS_KEY_SETTING.getKey() +
120+
" must be accompanied by setting [" + S3Repository.SECRET_KEY_SETTING.getKey() + "]");
121+
}
122+
try (SecureString key = S3Repository.ACCESS_KEY_SETTING.get(repositorySettings);
123+
SecureString secret = S3Repository.SECRET_KEY_SETTING.get(repositorySettings)) {
124+
credentials = new BasicAWSCredentials(key.toString(), secret.toString());
125+
}
126+
// backcompat for reading keys out of repository settings
127+
deprecationLogger.deprecated("Using s3 access/secret key from repository settings. Instead " +
128+
"store these in named clients and the elasticsearch keystore for secure settings.");
129+
} else if (S3Repository.SECRET_KEY_SETTING.exists(repositorySettings)) {
130+
throw new IllegalArgumentException("Repository setting [" + S3Repository.SECRET_KEY_SETTING.getKey() +
131+
" must be accompanied by setting [" + S3Repository.ACCESS_KEY_SETTING.getKey() + "]");
132+
}
133+
if (credentials == null) {
111134
logger.debug("Using instance profile credentials");
112135
return new PrivilegedInstanceProfileCredentialsProvider();
113136
} else {
114137
logger.debug("Using basic key/secret credentials");
115-
return new StaticCredentialsProvider(clientSettings.credentials);
138+
return new StaticCredentialsProvider(credentials);
139+
}
140+
}
141+
142+
/** Returns the value for a given setting from the repository, or returns the fallback value. */
143+
private static <T> T getRepoValue(Settings repositorySettings, Setting<T> repositorySetting, T fallback) {
144+
if (repositorySetting.exists(repositorySettings)) {
145+
return repositorySetting.get(repositorySettings);
116146
}
147+
return fallback;
117148
}
118149

119150
@Override

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121

2222
import java.io.IOException;
2323

24-
import com.amazonaws.ClientConfiguration;
2524
import com.amazonaws.services.s3.AmazonS3;
2625
import org.elasticsearch.cluster.metadata.RepositoryMetaData;
2726
import org.elasticsearch.common.Strings;
2827
import org.elasticsearch.common.blobstore.BlobPath;
2928
import org.elasticsearch.common.blobstore.BlobStore;
29+
import org.elasticsearch.common.settings.SecureSetting;
30+
import org.elasticsearch.common.settings.SecureString;
3031
import org.elasticsearch.common.settings.Setting;
31-
import org.elasticsearch.common.settings.Setting.Property;
3232
import org.elasticsearch.common.settings.Settings;
3333
import org.elasticsearch.common.unit.ByteSizeUnit;
3434
import org.elasticsearch.common.unit.ByteSizeValue;
@@ -53,6 +53,12 @@ class S3Repository extends BlobStoreRepository {
5353

5454
static final String TYPE = "s3";
5555

56+
/** The access key to authenticate with s3. This setting is insecure because cluster settings are stored in cluster state */
57+
static final Setting<SecureString> ACCESS_KEY_SETTING = SecureSetting.insecureString("access_key");
58+
59+
/** The secret key to authenticate with s3. This setting is insecure because cluster settings are stored in cluster state */
60+
static final Setting<SecureString> SECRET_KEY_SETTING = SecureSetting.insecureString("secret_key");
61+
5662
/**
5763
* Default is to use 100MB (S3 defaults) for heaps above 2GB and 5% of
5864
* the available memory for smaller heaps.

plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/AwsS3ServiceImplTests.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,21 @@
2424
import com.amazonaws.auth.AWSCredentials;
2525
import com.amazonaws.auth.AWSCredentialsProvider;
2626
import org.elasticsearch.common.settings.MockSecureSettings;
27+
import org.elasticsearch.common.settings.SecureSetting;
2728
import org.elasticsearch.common.settings.Setting;
2829
import org.elasticsearch.common.settings.Settings;
2930
import org.elasticsearch.test.ESTestCase;
3031

32+
import static org.hamcrest.Matchers.containsString;
3133
import static org.hamcrest.Matchers.instanceOf;
3234
import static org.hamcrest.Matchers.is;
3335

3436
public class AwsS3ServiceImplTests extends ESTestCase {
3537

3638
public void testAWSCredentialsWithSystemProviders() {
3739
S3ClientSettings clientSettings = S3ClientSettings.getClientSettings(Settings.EMPTY, "default");
38-
AWSCredentialsProvider credentialsProvider = InternalAwsS3Service.buildCredentials(logger, clientSettings);
40+
AWSCredentialsProvider credentialsProvider =
41+
InternalAwsS3Service.buildCredentials(logger, deprecationLogger, clientSettings, Settings.EMPTY);
3942
assertThat(credentialsProvider, instanceOf(InternalAwsS3Service.PrivilegedInstanceProfileCredentialsProvider.class));
4043
}
4144

@@ -44,7 +47,7 @@ public void testAwsCredsDefaultSettings() {
4447
secureSettings.setString("s3.client.default.access_key", "aws_key");
4548
secureSettings.setString("s3.client.default.secret_key", "aws_secret");
4649
Settings settings = Settings.builder().setSecureSettings(secureSettings).build();
47-
launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "aws_key", "aws_secret");
50+
assertCredentials(Settings.EMPTY, settings, "aws_key", "aws_secret");
4851
}
4952

5053
public void testAwsCredsExplicitConfigSettings() {
@@ -55,14 +58,38 @@ public void testAwsCredsExplicitConfigSettings() {
5558
secureSettings.setString("s3.client.default.access_key", "wrong_key");
5659
secureSettings.setString("s3.client.default.secret_key", "wrong_secret");
5760
Settings settings = Settings.builder().setSecureSettings(secureSettings).build();
58-
launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "aws_key", "aws_secret");
61+
assertCredentials(repositorySettings, settings, "aws_key", "aws_secret");
5962
}
6063

61-
private void launchAWSCredentialsWithElasticsearchSettingsTest(Settings singleRepositorySettings, Settings settings,
62-
String expectedKey, String expectedSecret) {
64+
public void testRepositorySettingsCredentialsDisallowed() {
65+
Settings repositorySettings = Settings.builder()
66+
.put(S3Repository.ACCESS_KEY_SETTING.getKey(), "aws_key")
67+
.put(S3Repository.SECRET_KEY_SETTING.getKey(), "aws_secret").build();
68+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->
69+
assertCredentials(repositorySettings, Settings.EMPTY, "aws_key", "aws_secret"));
70+
assertThat(e.getMessage(), containsString("Setting [access_key] is insecure"));
71+
}
72+
73+
public void testRepositorySettingsCredentialsMissingKey() {
74+
Settings repositorySettings = Settings.builder().put(S3Repository.SECRET_KEY_SETTING.getKey(), "aws_secret").build();
75+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->
76+
assertCredentials(repositorySettings, Settings.EMPTY, "aws_key", "aws_secret"));
77+
assertThat(e.getMessage(), containsString("must be accompanied by setting [access_key]"));
78+
}
79+
80+
public void testRepositorySettingsCredentialsMissingSecret() {
81+
Settings repositorySettings = Settings.builder().put(S3Repository.ACCESS_KEY_SETTING.getKey(), "aws_key").build();
82+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->
83+
assertCredentials(repositorySettings, Settings.EMPTY, "aws_key", "aws_secret"));
84+
assertThat(e.getMessage(), containsString("must be accompanied by setting [secret_key]"));
85+
}
86+
87+
private void assertCredentials(Settings singleRepositorySettings, Settings settings,
88+
String expectedKey, String expectedSecret) {
6389
String configName = InternalAwsS3Service.CLIENT_NAME.get(singleRepositorySettings);
6490
S3ClientSettings clientSettings = S3ClientSettings.getClientSettings(settings, configName);
65-
AWSCredentials credentials = InternalAwsS3Service.buildCredentials(logger, clientSettings).getCredentials();
91+
AWSCredentials credentials = InternalAwsS3Service.buildCredentials(logger, deprecationLogger,
92+
clientSettings, singleRepositorySettings).getCredentials();
6693
assertThat(credentials.getAWSAccessKeyId(), is(expectedKey));
6794
assertThat(credentials.getAWSSecretKey(), is(expectedSecret));
6895
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.repositories.s3;
21+
22+
import com.amazonaws.auth.AWSCredentials;
23+
import org.elasticsearch.common.settings.Setting;
24+
import org.elasticsearch.common.settings.Settings;
25+
import org.elasticsearch.test.ESTestCase;
26+
27+
public class RepositorySettingsCredentialsTests extends ESTestCase {
28+
29+
public void testRepositorySettingsCredentials() {
30+
Settings repositorySettings = Settings.builder()
31+
.put(S3Repository.ACCESS_KEY_SETTING.getKey(), "aws_key")
32+
.put(S3Repository.SECRET_KEY_SETTING.getKey(), "aws_secret").build();
33+
AWSCredentials credentials = InternalAwsS3Service.buildCredentials(logger, deprecationLogger,
34+
S3ClientSettings.getClientSettings(Settings.EMPTY, "default"), repositorySettings).getCredentials();
35+
assertEquals("aws_key", credentials.getAWSAccessKeyId());
36+
assertEquals("aws_secret", credentials.getAWSSecretKey());
37+
assertSettingDeprecationsAndWarnings(new Setting<?>[] { S3Repository.ACCESS_KEY_SETTING, S3Repository.SECRET_KEY_SETTING },
38+
"Using s3 access/secret key from repository settings. " +
39+
"Instead store these in named clients and the elasticsearch keystore for secure settings.");
40+
}
41+
}

0 commit comments

Comments
 (0)