Skip to content

Commit 6f70066

Browse files
Reintroduce the ability to configure S3 repository credentials in cluster state (#88652)
Revert of #46147, we want to keep this functionality around for a little longer.
1 parent 584f7f2 commit 6f70066

File tree

8 files changed

+253
-9
lines changed

8 files changed

+253
-9
lines changed

modules/repository-s3/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ esplugin.bundleSpec.from('config/repository-s3') {
7575
into 'config'
7676
}
7777

78+
def testRepositoryCreds = tasks.register("testRepositoryCreds", Test) {
79+
include '**/RepositoryCredentialsTests.class'
80+
systemProperty 'es.allow_insecure_settings', 'true'
81+
}
82+
83+
tasks.named('check').configure {
84+
dependsOn(testRepositoryCreds)
85+
}
86+
87+
tasks.named('test').configure {
88+
// this is tested explicitly in separate test tasks
89+
exclude '**/RepositoryCredentialsTests.class'
90+
}
91+
7892
boolean useFixture = false
7993

8094
def fixtureAddress = { fixture, name, port ->

modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3ClientSettings.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ S3ClientSettings refine(Settings repositorySettings) {
263263
normalizedSettings,
264264
disableChunkedEncoding
265265
);
266+
final S3BasicCredentials newCredentials;
267+
if (checkDeprecatedCredentials(repositorySettings)) {
268+
newCredentials = loadDeprecatedCredentials(repositorySettings);
269+
} else {
270+
newCredentials = credentials;
271+
}
266272
final String newRegion = getRepoSettingOrDefault(REGION, normalizedSettings, region);
267273
final String newSignerOverride = getRepoSettingOrDefault(SIGNER_OVERRIDE, normalizedSettings, signerOverride);
268274
if (Objects.equals(endpoint, newEndpoint)
@@ -272,14 +278,15 @@ S3ClientSettings refine(Settings repositorySettings) {
272278
&& newReadTimeoutMillis == readTimeoutMillis
273279
&& maxRetries == newMaxRetries
274280
&& newThrottleRetries == throttleRetries
281+
&& Objects.equals(credentials, newCredentials)
275282
&& newPathStyleAccess == pathStyleAccess
276283
&& newDisableChunkedEncoding == disableChunkedEncoding
277284
&& Objects.equals(region, newRegion)
278285
&& Objects.equals(signerOverride, newSignerOverride)) {
279286
return this;
280287
}
281288
return new S3ClientSettings(
282-
credentials,
289+
newCredentials,
283290
newEndpoint,
284291
newProtocol,
285292
newProxyHost,
@@ -315,6 +322,41 @@ static Map<String, S3ClientSettings> load(Settings settings) {
315322
return Collections.unmodifiableMap(clients);
316323
}
317324

325+
static boolean checkDeprecatedCredentials(Settings repositorySettings) {
326+
if (S3Repository.ACCESS_KEY_SETTING.exists(repositorySettings)) {
327+
if (S3Repository.SECRET_KEY_SETTING.exists(repositorySettings) == false) {
328+
throw new IllegalArgumentException(
329+
"Repository setting ["
330+
+ S3Repository.ACCESS_KEY_SETTING.getKey()
331+
+ " must be accompanied by setting ["
332+
+ S3Repository.SECRET_KEY_SETTING.getKey()
333+
+ "]"
334+
);
335+
}
336+
return true;
337+
} else if (S3Repository.SECRET_KEY_SETTING.exists(repositorySettings)) {
338+
throw new IllegalArgumentException(
339+
"Repository setting ["
340+
+ S3Repository.SECRET_KEY_SETTING.getKey()
341+
+ " must be accompanied by setting ["
342+
+ S3Repository.ACCESS_KEY_SETTING.getKey()
343+
+ "]"
344+
);
345+
}
346+
return false;
347+
}
348+
349+
// backcompat for reading keys out of repository settings (clusterState)
350+
private static S3BasicCredentials loadDeprecatedCredentials(Settings repositorySettings) {
351+
assert checkDeprecatedCredentials(repositorySettings);
352+
try (
353+
SecureString key = S3Repository.ACCESS_KEY_SETTING.get(repositorySettings);
354+
SecureString secret = S3Repository.SECRET_KEY_SETTING.get(repositorySettings)
355+
) {
356+
return new S3BasicCredentials(key.toString(), secret.toString());
357+
}
358+
}
359+
318360
private static S3BasicCredentials loadCredentials(Settings settings, String clientName) {
319361
try (
320362
SecureString accessKey = getConfigValue(settings, clientName, ACCESS_KEY_SETTING);

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
import org.elasticsearch.common.Strings;
1919
import org.elasticsearch.common.blobstore.BlobPath;
2020
import org.elasticsearch.common.blobstore.BlobStore;
21+
import org.elasticsearch.common.logging.DeprecationCategory;
22+
import org.elasticsearch.common.logging.DeprecationLogger;
23+
import org.elasticsearch.common.settings.SecureSetting;
24+
import org.elasticsearch.common.settings.SecureString;
2125
import org.elasticsearch.common.settings.Setting;
2226
import org.elasticsearch.common.unit.ByteSizeUnit;
2327
import org.elasticsearch.common.unit.ByteSizeValue;
@@ -57,9 +61,16 @@
5761
*/
5862
class S3Repository extends MeteredBlobStoreRepository {
5963
private static final Logger logger = LogManager.getLogger(S3Repository.class);
64+
private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(logger.getName());
6065

6166
static final String TYPE = "s3";
6267

68+
/** The access key to authenticate with s3. This setting is insecure because cluster settings are stored in cluster state */
69+
static final Setting<SecureString> ACCESS_KEY_SETTING = SecureSetting.insecureString("access_key");
70+
71+
/** The secret key to authenticate with s3. This setting is insecure because cluster settings are stored in cluster state */
72+
static final Setting<SecureString> SECRET_KEY_SETTING = SecureSetting.insecureString("secret_key");
73+
6374
/**
6475
* Default is to use 100MB (S3 defaults) for heaps above 2GB and 5% of
6576
* the available memory for smaller heaps.
@@ -233,6 +244,16 @@ class S3Repository extends MeteredBlobStoreRepository {
233244
this.storageClass = STORAGE_CLASS_SETTING.get(metadata.settings());
234245
this.cannedACL = CANNED_ACL_SETTING.get(metadata.settings());
235246

247+
if (S3ClientSettings.checkDeprecatedCredentials(metadata.settings())) {
248+
// provided repository settings
249+
deprecationLogger.critical(
250+
DeprecationCategory.SECURITY,
251+
"s3_repository_secret_settings",
252+
"Using s3 access/secret key from repository settings. Instead "
253+
+ "store these in named clients and the elasticsearch keystore for secure settings."
254+
);
255+
}
256+
236257
coolDown = COOLDOWN_PERIOD.get(metadata.settings());
237258

238259
logger.debug(

modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ public List<Setting<?>> getSettings() {
141141
S3ClientSettings.USE_THROTTLE_RETRIES_SETTING,
142142
S3ClientSettings.USE_PATH_STYLE_ACCESS,
143143
S3ClientSettings.SIGNER_OVERRIDE,
144-
S3ClientSettings.REGION
144+
S3ClientSettings.REGION,
145+
S3Repository.ACCESS_KEY_SETTING,
146+
S3Repository.SECRET_KEY_SETTING
145147
);
146148
}
147149

modules/repository-s3/src/main/plugin-metadata/plugin-security.policy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ grant {
2828
// s3 client opens socket connections for to access repository
2929
permission java.net.SocketPermission "*", "connect";
3030

31-
31+
// only for tests : org.elasticsearch.repositories.s3.S3RepositoryPlugin
32+
permission java.util.PropertyPermission "es.allow_insecure_settings", "read,write";
3233
};

modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,54 @@
1414

1515
import org.apache.logging.log4j.LogManager;
1616
import org.apache.logging.log4j.Logger;
17+
import org.elasticsearch.client.internal.node.NodeClient;
1718
import org.elasticsearch.cluster.metadata.RepositoryMetadata;
1819
import org.elasticsearch.cluster.service.ClusterService;
1920
import org.elasticsearch.common.settings.MockSecureSettings;
2021
import org.elasticsearch.common.settings.Settings;
22+
import org.elasticsearch.common.settings.SettingsFilter;
2123
import org.elasticsearch.common.util.BigArrays;
24+
import org.elasticsearch.core.SuppressForbidden;
2225
import org.elasticsearch.env.Environment;
2326
import org.elasticsearch.indices.recovery.RecoverySettings;
2427
import org.elasticsearch.plugins.Plugin;
2528
import org.elasticsearch.plugins.PluginsService;
2629
import org.elasticsearch.repositories.RepositoriesService;
30+
import org.elasticsearch.rest.AbstractRestChannel;
31+
import org.elasticsearch.rest.RestRequest;
32+
import org.elasticsearch.rest.RestResponse;
33+
import org.elasticsearch.rest.action.admin.cluster.RestGetRepositoriesAction;
2734
import org.elasticsearch.test.ESSingleNodeTestCase;
35+
import org.elasticsearch.test.rest.FakeRestRequest;
2836
import org.elasticsearch.xcontent.NamedXContentRegistry;
2937

38+
import java.security.AccessController;
39+
import java.security.PrivilegedAction;
3040
import java.util.Collection;
3141
import java.util.List;
42+
import java.util.concurrent.CountDownLatch;
43+
import java.util.concurrent.atomic.AtomicReference;
3244

3345
import static org.elasticsearch.repositories.s3.S3ClientSettings.ACCESS_KEY_SETTING;
3446
import static org.elasticsearch.repositories.s3.S3ClientSettings.SECRET_KEY_SETTING;
3547
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
48+
import static org.hamcrest.Matchers.containsString;
3649
import static org.hamcrest.Matchers.instanceOf;
3750
import static org.hamcrest.Matchers.is;
51+
import static org.hamcrest.Matchers.not;
3852
import static org.hamcrest.Matchers.notNullValue;
3953

54+
@SuppressForbidden(reason = "test requires to set a System property to allow insecure settings when running in IDE")
4055
public class RepositoryCredentialsTests extends ESSingleNodeTestCase {
4156

57+
static {
58+
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
59+
// required for client settings overwriting when running in IDE
60+
System.setProperty("es.allow_insecure_settings", "true");
61+
return null;
62+
});
63+
}
64+
4265
@Override
4366
protected Collection<Class<? extends Plugin>> getPlugins() {
4467
return List.of(ProxyS3RepositoryPlugin.class);
@@ -60,11 +83,51 @@ protected Settings nodeSettings() {
6083
return Settings.builder().setSecureSettings(secureSettings).put(super.nodeSettings()).build();
6184
}
6285

86+
public void testRepositoryCredentialsOverrideSecureCredentials() {
87+
final String repositoryName = "repo-creds-override";
88+
final Settings.Builder repositorySettings = Settings.builder()
89+
// repository settings for credentials override node secure settings
90+
.put(S3Repository.ACCESS_KEY_SETTING.getKey(), "insecure_aws_key")
91+
.put(S3Repository.SECRET_KEY_SETTING.getKey(), "insecure_aws_secret");
92+
93+
final String clientName = randomFrom("default", "other", null);
94+
if (clientName != null) {
95+
repositorySettings.put(S3Repository.CLIENT_NAME.getKey(), clientName);
96+
}
97+
createRepository(repositoryName, repositorySettings.build());
98+
99+
final RepositoriesService repositories = getInstanceFromNode(RepositoriesService.class);
100+
assertThat(repositories.repository(repositoryName), notNullValue());
101+
assertThat(repositories.repository(repositoryName), instanceOf(S3Repository.class));
102+
103+
final S3Repository repository = (S3Repository) repositories.repository(repositoryName);
104+
final AmazonS3 client = repository.createBlobStore().clientReference().client();
105+
assertThat(client, instanceOf(ProxyS3RepositoryPlugin.ClientAndCredentials.class));
106+
107+
final AWSCredentials credentials = ((ProxyS3RepositoryPlugin.ClientAndCredentials) client).credentials.getCredentials();
108+
assertThat(credentials.getAWSAccessKeyId(), is("insecure_aws_key"));
109+
assertThat(credentials.getAWSSecretKey(), is("insecure_aws_secret"));
110+
111+
assertCriticalWarnings(
112+
"[secret_key] setting was deprecated in Elasticsearch and will be removed in a future release.",
113+
"Using s3 access/secret key from repository settings. Instead store these in named clients and"
114+
+ " the elasticsearch keystore for secure settings.",
115+
"[access_key] setting was deprecated in Elasticsearch and will be removed in a future release."
116+
);
117+
}
118+
63119
public void testReinitSecureCredentials() {
64120
final String clientName = randomFrom("default", "other");
65121

66122
final Settings.Builder repositorySettings = Settings.builder();
67-
repositorySettings.put(S3Repository.CLIENT_NAME.getKey(), clientName);
123+
final boolean hasInsecureSettings = randomBoolean();
124+
if (hasInsecureSettings) {
125+
// repository settings for credentials override node secure settings
126+
repositorySettings.put(S3Repository.ACCESS_KEY_SETTING.getKey(), "insecure_aws_key");
127+
repositorySettings.put(S3Repository.SECRET_KEY_SETTING.getKey(), "insecure_aws_secret");
128+
} else {
129+
repositorySettings.put(S3Repository.CLIENT_NAME.getKey(), clientName);
130+
}
68131

69132
final String repositoryName = "repo-reinit-creds";
70133
createRepository(repositoryName, repositorySettings.build());
@@ -79,7 +142,10 @@ public void testReinitSecureCredentials() {
79142
assertThat(client, instanceOf(ProxyS3RepositoryPlugin.ClientAndCredentials.class));
80143

81144
final AWSCredentials credentials = ((ProxyS3RepositoryPlugin.ClientAndCredentials) client).credentials.getCredentials();
82-
if ("other".equals(clientName)) {
145+
if (hasInsecureSettings) {
146+
assertThat(credentials.getAWSAccessKeyId(), is("insecure_aws_key"));
147+
assertThat(credentials.getAWSSecretKey(), is("insecure_aws_secret"));
148+
} else if ("other".equals(clientName)) {
83149
assertThat(credentials.getAWSAccessKeyId(), is("secure_other_key"));
84150
assertThat(credentials.getAWSSecretKey(), is("secure_other_secret"));
85151
} else {
@@ -98,7 +164,10 @@ public void testReinitSecureCredentials() {
98164
plugin.reload(newSettings);
99165

100166
// check the not-yet-closed client reference still has the same credentials
101-
if ("other".equals(clientName)) {
167+
if (hasInsecureSettings) {
168+
assertThat(credentials.getAWSAccessKeyId(), is("insecure_aws_key"));
169+
assertThat(credentials.getAWSSecretKey(), is("insecure_aws_secret"));
170+
} else if ("other".equals(clientName)) {
102171
assertThat(credentials.getAWSAccessKeyId(), is("secure_other_key"));
103172
assertThat(credentials.getAWSSecretKey(), is("secure_other_secret"));
104173
} else {
@@ -113,11 +182,66 @@ public void testReinitSecureCredentials() {
113182
assertThat(client, instanceOf(ProxyS3RepositoryPlugin.ClientAndCredentials.class));
114183

115184
final AWSCredentials newCredentials = ((ProxyS3RepositoryPlugin.ClientAndCredentials) client).credentials.getCredentials();
116-
assertThat(newCredentials.getAWSAccessKeyId(), is("new_secret_aws_key"));
117-
assertThat(newCredentials.getAWSSecretKey(), is("new_secret_aws_secret"));
185+
if (hasInsecureSettings) {
186+
assertThat(newCredentials.getAWSAccessKeyId(), is("insecure_aws_key"));
187+
assertThat(newCredentials.getAWSSecretKey(), is("insecure_aws_secret"));
188+
} else {
189+
assertThat(newCredentials.getAWSAccessKeyId(), is("new_secret_aws_key"));
190+
assertThat(newCredentials.getAWSSecretKey(), is("new_secret_aws_secret"));
191+
}
192+
}
193+
194+
if (hasInsecureSettings) {
195+
assertCriticalWarnings(
196+
"[secret_key] setting was deprecated in Elasticsearch and will be removed in a future release.",
197+
"Using s3 access/secret key from repository settings. Instead store these in named clients and"
198+
+ " the elasticsearch keystore for secure settings.",
199+
"[access_key] setting was deprecated in Elasticsearch and will be removed in a future release."
200+
);
118201
}
119202
}
120203

204+
public void testInsecureRepositoryCredentials() throws Exception {
205+
final String repositoryName = "repo-insecure-creds";
206+
createRepository(
207+
repositoryName,
208+
Settings.builder()
209+
.put(S3Repository.ACCESS_KEY_SETTING.getKey(), "insecure_aws_key")
210+
.put(S3Repository.SECRET_KEY_SETTING.getKey(), "insecure_aws_secret")
211+
.build()
212+
);
213+
214+
final RestRequest fakeRestRequest = new FakeRestRequest();
215+
fakeRestRequest.params().put("repository", repositoryName);
216+
final RestGetRepositoriesAction action = new RestGetRepositoriesAction(getInstanceFromNode(SettingsFilter.class));
217+
218+
final CountDownLatch latch = new CountDownLatch(1);
219+
final AtomicReference<AssertionError> error = new AtomicReference<>();
220+
action.handleRequest(fakeRestRequest, new AbstractRestChannel(fakeRestRequest, true) {
221+
@Override
222+
public void sendResponse(RestResponse response) {
223+
try {
224+
String responseAsString = response.content().utf8ToString();
225+
assertThat(responseAsString, containsString(repositoryName));
226+
assertThat(responseAsString, not(containsString("insecure_")));
227+
} catch (final AssertionError ex) {
228+
error.set(ex);
229+
}
230+
latch.countDown();
231+
}
232+
}, getInstanceFromNode(NodeClient.class));
233+
234+
latch.await();
235+
if (error.get() != null) {
236+
throw error.get();
237+
}
238+
239+
assertWarnings(
240+
"Using s3 access/secret key from repository settings. Instead store these in named clients and"
241+
+ " the elasticsearch keystore for secure settings."
242+
);
243+
}
244+
121245
private void createRepository(final String name, final Settings repositorySettings) {
122246
assertAcked(
123247
client().admin()

0 commit comments

Comments
 (0)