Skip to content

Commit cd72f45

Browse files
Client-side encrypted snapshot repository (feature flag) (#66773)
The client-side encrypted repository is a new type of snapshot repository that internally delegates to the regular variants of snapshot repositories (of types Azure, S3, GCS, FS, and maybe others but not yet tested). After the encrypted repository is set up, it is transparent to the snapshot and restore APIs (i.e. all snapshots stored in the encrypted repository are encrypted, no other parameters required). The encrypted repository is protected by a password stored on every node's keystore (which must be the same across the nodes). The password is used to generate a key encrytion key (KEK), using the PBKDF2 function, which is used to encrypt (using the AES Wrap algorithm) other symmetric keys (referred to as DEK - data encryption keys), which themselves are generated randomly, and which are ultimately used to encrypt the snapshot blobs. For example, here is how to set up an encrypted FS repository: ------ 1) make sure that the cluster runs under at least a "platinum" license (simplest test configuration is to put `xpack.license.self_generated.type: "trial"` in the elasticsearch.yml file) 2) identical to the un-encrypted FS repository, specify the mount point of the shared FS in the elasticsearch.yml conf file (on all the cluster nodes), e.g. `path.repo: ["/tmp/repo"]` 3) store the repository password inside the elasticsearch.keystore, *on every cluster node*. In order to support changing password on existing repository (implemented in a follow-up), the password itself must be names, e.g. for the "test_enc_key" repository password name: `./bin/elasticsearch-keystore add repository.encrypted.test_enc_pass.password` *type in the password* 4) start up the cluster and create the new encrypted FS repository, named "test_enc", by calling: ` curl -X PUT "localhost:9200/_snapshot/test_enc?pretty" -H 'Content-Type: application/json' -d' { "type": "encrypted", "settings": { "location": "/tmp/repo/enc", "delegate_type": "fs", "password_name": "test_enc_pass" } } ' ` 5) the snapshot and restore APIs work unmodified when they refer to this new repository, e.g. ` curl -X PUT "localhost:9200/_snapshot/test_enc/snapshot_1?wait_for_completion=true"` Related: #49896 #41910 #50846 #48221 #65768
1 parent dd1ffe3 commit cd72f45

File tree

42 files changed

+7956
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+7956
-80
lines changed

plugins/repository-azure/build.gradle

+15
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,18 @@ task azureThirdPartyTest(type: Test) {
374374
}
375375
}
376376
tasks.named("check").configure { dependsOn("azureThirdPartyTest") }
377+
378+
// test jar is exported by the integTestArtifacts configuration to be used in the encrypted Azure repository test
379+
configurations {
380+
internalClusterTestArtifacts.extendsFrom internalClusterTestImplementation
381+
internalClusterTestArtifacts.extendsFrom internalClusterTestRuntime
382+
}
383+
384+
def internalClusterTestJar = tasks.register("internalClusterTestJar", Jar) {
385+
appendix 'internalClusterTest'
386+
from sourceSets.internalClusterTest.output
387+
}
388+
389+
artifacts {
390+
internalClusterTestArtifacts internalClusterTestJar
391+
}

plugins/repository-azure/src/internalClusterTest/java/org/elasticsearch/repositories/azure/AzureBlobStoreRepositoryTests.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,15 @@ protected String repositoryType() {
5959
}
6060

6161
@Override
62-
protected Settings repositorySettings() {
63-
return Settings.builder()
64-
.put(super.repositorySettings())
65-
.put(AzureRepository.Repository.CONTAINER_SETTING.getKey(), "container")
66-
.put(AzureStorageSettings.ACCOUNT_SETTING.getKey(), "test")
67-
.build();
62+
protected Settings repositorySettings(String repoName) {
63+
Settings.Builder settingsBuilder = Settings.builder()
64+
.put(super.repositorySettings(repoName))
65+
.put(AzureRepository.Repository.CONTAINER_SETTING.getKey(), "container")
66+
.put(AzureStorageSettings.ACCOUNT_SETTING.getKey(), "test");
67+
if (randomBoolean()) {
68+
settingsBuilder.put(AzureRepository.Repository.BASE_PATH_SETTING.getKey(), randomFrom("test", "test/1"));
69+
}
70+
return settingsBuilder.build();
6871
}
6972

7073
@Override

plugins/repository-gcs/build.gradle

+17
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,20 @@ def gcsThirdPartyTest = tasks.register("gcsThirdPartyTest", Test) {
334334
tasks.named("check").configure {
335335
dependsOn(largeBlobYamlRestTest, gcsThirdPartyTest)
336336
}
337+
338+
// test jar is exported by the integTestArtifacts configuration to be used in the encrypted GCS repository test
339+
configurations {
340+
internalClusterTestArtifacts.extendsFrom internalClusterTestImplementation
341+
internalClusterTestArtifacts.extendsFrom internalClusterTestRuntime
342+
}
343+
344+
def internalClusterTestJar = tasks.register("internalClusterTestJar", Jar) {
345+
appendix 'internalClusterTest'
346+
from sourceSets.internalClusterTest.output
347+
// for the repositories.gcs.TestUtils class
348+
from sourceSets.test.output
349+
}
350+
351+
artifacts {
352+
internalClusterTestArtifacts internalClusterTestJar
353+
}

plugins/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java

+11-7
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.CREDENTIALS_FILE_SETTING;
6868
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.ENDPOINT_SETTING;
6969
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.TOKEN_URI_SETTING;
70+
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BASE_PATH;
7071
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BUCKET;
7172
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.CLIENT_NAME;
7273

@@ -79,12 +80,15 @@ protected String repositoryType() {
7980
}
8081

8182
@Override
82-
protected Settings repositorySettings() {
83-
return Settings.builder()
84-
.put(super.repositorySettings())
85-
.put(BUCKET.getKey(), "bucket")
86-
.put(CLIENT_NAME.getKey(), "test")
87-
.build();
83+
protected Settings repositorySettings(String repoName) {
84+
Settings.Builder settingsBuilder = Settings.builder()
85+
.put(super.repositorySettings(repoName))
86+
.put(BUCKET.getKey(), "bucket")
87+
.put(CLIENT_NAME.getKey(), "test");
88+
if (randomBoolean()) {
89+
settingsBuilder.put(BASE_PATH.getKey(), randomFrom("test", "test/1"));
90+
}
91+
return settingsBuilder.build();
8892
}
8993

9094
@Override
@@ -120,7 +124,7 @@ protected Settings nodeSettings(int nodeOrdinal) {
120124
}
121125

122126
public void testDeleteSingleItem() {
123-
final String repoName = createRepository(randomName());
127+
final String repoName = createRepository(randomRepositoryName());
124128
final RepositoriesService repositoriesService = internalCluster().getMasterNodeInstance(RepositoriesService.class);
125129
final BlobStoreRepository repository = (BlobStoreRepository) repositoriesService.repository(repoName);
126130
PlainActionFuture.get(f -> repository.threadPool().generic().execute(ActionRunnable.run(f, () ->

plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protected String repositoryType() {
3838
}
3939

4040
@Override
41-
protected Settings repositorySettings() {
41+
protected Settings repositorySettings(String repoName) {
4242
return Settings.builder()
4343
.put("uri", "hdfs:///")
4444
.put("conf.fs.AbstractFileSystem.hdfs.impl", TestingFs.class.getName())
@@ -47,6 +47,12 @@ protected Settings repositorySettings() {
4747
.put("compress", randomBoolean()).build();
4848
}
4949

50+
@Override
51+
public void testSnapshotAndRestore() throws Exception {
52+
// the HDFS mockup doesn't preserve the repository contents after removing the repository
53+
testSnapshotAndRestore(false);
54+
}
55+
5056
@Override
5157
protected Collection<Class<? extends Plugin>> nodePlugins() {
5258
return Collections.singletonList(HdfsPlugin.class);

plugins/repository-s3/build.gradle

+17
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,20 @@ tasks.named("thirdPartyAudit").configure {
312312
'javax.activation.DataHandler'
313313
)
314314
}
315+
316+
// test jar is exported by the integTestArtifacts configuration to be used in the encrypted S3 repository test
317+
configurations {
318+
internalClusterTestArtifacts.extendsFrom internalClusterTestImplementation
319+
internalClusterTestArtifacts.extendsFrom internalClusterTestRuntime
320+
}
321+
322+
def internalClusterTestJar = tasks.register("internalClusterTestJar", Jar) {
323+
appendix 'internalClusterTest'
324+
from sourceSets.internalClusterTest.output
325+
// for the plugin-security.policy resource
326+
from sourceSets.test.output
327+
}
328+
329+
artifacts {
330+
internalClusterTestArtifacts internalClusterTestJar
331+
}

plugins/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java

+11-7
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,17 @@ protected String repositoryType() {
9494
}
9595

9696
@Override
97-
protected Settings repositorySettings() {
98-
return Settings.builder()
99-
.put(super.repositorySettings())
97+
protected Settings repositorySettings(String repoName) {
98+
Settings.Builder settingsBuilder = Settings.builder()
99+
.put(super.repositorySettings(repoName))
100100
.put(S3Repository.BUCKET_SETTING.getKey(), "bucket")
101101
.put(S3Repository.CLIENT_NAME.getKey(), "test")
102102
// Don't cache repository data because some tests manually modify the repository data
103-
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
104-
.build();
103+
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false);
104+
if (randomBoolean()) {
105+
settingsBuilder.put(S3Repository.BASE_PATH_SETTING.getKey(), randomFrom("test", "test/1"));
106+
}
107+
return settingsBuilder.build();
105108
}
106109

107110
@Override
@@ -145,8 +148,9 @@ protected Settings nodeSettings(int nodeOrdinal) {
145148
}
146149

147150
public void testEnforcedCooldownPeriod() throws IOException {
148-
final String repoName = createRepository(randomName(), Settings.builder().put(repositorySettings())
149-
.put(S3Repository.COOLDOWN_PERIOD.getKey(), TEST_COOLDOWN_PERIOD).build());
151+
final String repoName = randomRepositoryName();
152+
createRepository(repoName, Settings.builder().put(repositorySettings(repoName))
153+
.put(S3Repository.COOLDOWN_PERIOD.getKey(), TEST_COOLDOWN_PERIOD).build(), true);
150154

151155
final SnapshotId fakeOldSnapshot = client().admin().cluster().prepareCreateSnapshot(repoName, "snapshot-old")
152156
.setWaitForCompletion(true).setIndices().get().getSnapshotInfo().snapshotId();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.fs;
21+
22+
import org.elasticsearch.common.settings.Settings;
23+
import org.elasticsearch.common.unit.ByteSizeUnit;
24+
import org.elasticsearch.common.unit.ByteSizeValue;
25+
import org.elasticsearch.repositories.blobstore.ESFsBasedRepositoryIntegTestCase;
26+
27+
public class FsBlobStoreRepositoryIntegTests extends ESFsBasedRepositoryIntegTestCase {
28+
29+
@Override
30+
protected Settings repositorySettings(String repositoryName) {
31+
final Settings.Builder settings = Settings.builder()
32+
.put("compress", randomBoolean())
33+
.put("location", randomRepoPath());
34+
if (randomBoolean()) {
35+
long size = 1 << randomInt(10);
36+
settings.put("chunk_size", new ByteSizeValue(size, ByteSizeUnit.KB));
37+
}
38+
return settings.build();
39+
}
40+
}

server/src/main/java/org/elasticsearch/common/blobstore/BlobPath.java

+14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Collections;
2626
import java.util.Iterator;
2727
import java.util.List;
28+
import java.util.Objects;
2829

2930
/**
3031
* The list of paths where a blob can reside. The contents of the paths are dependent upon the implementation of {@link BlobContainer}.
@@ -90,4 +91,17 @@ public String toString() {
9091
}
9192
return sb.toString();
9293
}
94+
95+
@Override
96+
public boolean equals(Object o) {
97+
if (this == o) return true;
98+
if (o == null || getClass() != o.getClass()) return false;
99+
BlobPath other = (BlobPath) o;
100+
return paths.equals(other.paths);
101+
}
102+
103+
@Override
104+
public int hashCode() {
105+
return Objects.hash(paths);
106+
}
93107
}

test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java

+46-11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.elasticsearch.repositories.RepositoriesService;
4242
import org.elasticsearch.repositories.Repository;
4343
import org.elasticsearch.repositories.RepositoryData;
44+
import org.elasticsearch.repositories.RepositoryMissingException;
4445
import org.elasticsearch.snapshots.SnapshotMissingException;
4546
import org.elasticsearch.snapshots.SnapshotRestoreException;
4647
import org.elasticsearch.test.ESIntegTestCase;
@@ -78,17 +79,19 @@ public static RepositoryData getRepositoryData(Repository repository) {
7879

7980
protected abstract String repositoryType();
8081

81-
protected Settings repositorySettings() {
82+
protected Settings repositorySettings(String repoName) {
8283
return Settings.builder().put("compress", randomBoolean()).build();
8384
}
8485

8586
protected final String createRepository(final String name) {
86-
return createRepository(name, repositorySettings());
87+
return createRepository(name, true);
8788
}
8889

89-
protected final String createRepository(final String name, final Settings settings) {
90-
final boolean verify = randomBoolean();
90+
protected final String createRepository(final String name, final boolean verify) {
91+
return createRepository(name, repositorySettings(name), verify);
92+
}
9193

94+
protected final String createRepository(final String name, final Settings settings, final boolean verify) {
9295
logger.info("--> creating repository [name: {}, verify: {}, settings: {}]", name, verify, settings);
9396
assertAcked(client().admin().cluster().preparePutRepository(name)
9497
.setType(repositoryType())
@@ -98,14 +101,23 @@ protected final String createRepository(final String name, final Settings settin
98101
internalCluster().getDataOrMasterNodeInstances(RepositoriesService.class).forEach(repositories -> {
99102
assertThat(repositories.repository(name), notNullValue());
100103
assertThat(repositories.repository(name), instanceOf(BlobStoreRepository.class));
101-
assertThat(repositories.repository(name).isReadOnly(), is(false));
104+
assertThat(repositories.repository(name).isReadOnly(), is(settings.getAsBoolean("readonly", false)));
102105
BlobStore blobStore = ((BlobStoreRepository) repositories.repository(name)).getBlobStore();
103106
assertThat("blob store has to be lazy initialized", blobStore, verify ? is(notNullValue()) : is(nullValue()));
104107
});
105108

106109
return name;
107110
}
108111

112+
protected final void deleteRepository(final String name) {
113+
logger.debug("--> deleting repository [name: {}]", name);
114+
assertAcked(client().admin().cluster().prepareDeleteRepository(name));
115+
internalCluster().getDataOrMasterNodeInstances(RepositoriesService.class).forEach(repositories -> {
116+
RepositoryMissingException e = expectThrows(RepositoryMissingException.class, () -> repositories.repository(name));
117+
assertThat(e.repository(), equalTo(name));
118+
});
119+
}
120+
109121
public void testReadNonExistingPath() throws IOException {
110122
try (BlobStore store = newBlobStore()) {
111123
final BlobContainer container = store.blobContainer(new BlobPath());
@@ -176,7 +188,7 @@ public void testList() throws IOException {
176188
BlobMetadata blobMetadata = blobs.get(generated.getKey());
177189
assertThat(generated.getKey(), blobMetadata, CoreMatchers.notNullValue());
178190
assertThat(blobMetadata.name(), CoreMatchers.equalTo(generated.getKey()));
179-
assertThat(blobMetadata.length(), CoreMatchers.equalTo(generated.getValue()));
191+
assertThat(blobMetadata.length(), CoreMatchers.equalTo(blobLengthFromContentLength(generated.getValue())));
180192
}
181193

182194
assertThat(container.listBlobsByPrefix("foo-").size(), CoreMatchers.equalTo(numberOfFooBlobs));
@@ -259,15 +271,25 @@ protected static void writeBlob(BlobContainer container, String blobName, BytesA
259271
}
260272

261273
protected BlobStore newBlobStore() {
262-
final String repository = createRepository(randomName());
274+
final String repository = createRepository(randomRepositoryName());
275+
return newBlobStore(repository);
276+
}
277+
278+
protected BlobStore newBlobStore(String repository) {
263279
final BlobStoreRepository blobStoreRepository =
264280
(BlobStoreRepository) internalCluster().getMasterNodeInstance(RepositoriesService.class).repository(repository);
265281
return PlainActionFuture.get(
266282
f -> blobStoreRepository.threadPool().generic().execute(ActionRunnable.supply(f, blobStoreRepository::blobStore)));
267283
}
268284

269285
public void testSnapshotAndRestore() throws Exception {
270-
final String repoName = createRepository(randomName());
286+
testSnapshotAndRestore(randomBoolean());
287+
}
288+
289+
protected void testSnapshotAndRestore(boolean recreateRepositoryBeforeRestore) throws Exception {
290+
final String repoName = randomRepositoryName();
291+
final Settings repoSettings = repositorySettings(repoName);
292+
createRepository(repoName, repoSettings, randomBoolean());
271293
int indexCount = randomIntBetween(1, 5);
272294
int[] docCounts = new int[indexCount];
273295
String[] indexNames = generateRandomNames(indexCount);
@@ -315,6 +337,11 @@ public void testSnapshotAndRestore() throws Exception {
315337
assertAcked(client().admin().indices().prepareClose(closeIndices.toArray(new String[closeIndices.size()])));
316338
}
317339

340+
if (recreateRepositoryBeforeRestore) {
341+
deleteRepository(repoName);
342+
createRepository(repoName, repoSettings, randomBoolean());
343+
}
344+
318345
logger.info("--> restore all indices from the snapshot");
319346
assertSuccessfulRestore(client().admin().cluster().prepareRestoreSnapshot(repoName, snapshotName).setWaitForCompletion(true));
320347

@@ -339,7 +366,7 @@ public void testSnapshotAndRestore() throws Exception {
339366
}
340367

341368
public void testMultipleSnapshotAndRollback() throws Exception {
342-
final String repoName = createRepository(randomName());
369+
final String repoName = createRepository(randomRepositoryName());
343370
int iterationCount = randomIntBetween(2, 5);
344371
int[] docCounts = new int[iterationCount];
345372
String indexName = randomName();
@@ -394,7 +421,7 @@ public void testMultipleSnapshotAndRollback() throws Exception {
394421
}
395422

396423
public void testIndicesDeletedFromRepository() throws Exception {
397-
final String repoName = createRepository("test-repo");
424+
final String repoName = createRepository(randomRepositoryName());
398425
Client client = client();
399426
createIndex("test-idx-1", "test-idx-2", "test-idx-3");
400427
ensureGreen();
@@ -491,7 +518,15 @@ private static void assertSuccessfulRestore(RestoreSnapshotResponse response) {
491518
assertThat(response.getRestoreInfo().successfulShards(), equalTo(response.getRestoreInfo().totalShards()));
492519
}
493520

494-
protected static String randomName() {
521+
protected String randomName() {
495522
return randomAlphaOfLength(randomIntBetween(1, 10)).toLowerCase(Locale.ROOT);
496523
}
524+
525+
protected String randomRepositoryName() {
526+
return randomName();
527+
}
528+
529+
protected long blobLengthFromContentLength(long contentLength) {
530+
return contentLength;
531+
}
497532
}

0 commit comments

Comments
 (0)