Skip to content

Commit

Permalink
Encrypted blob store reuse DEK (#53352)
Browse files Browse the repository at this point in the history
This builds upon the data encryption streams from #49896
to create an encrypted snapshot repository.
The repository encryption works with the following existing repository types:
FS, Azure, S3, GCS (possibly works with HDFS and URL, but these are not tested).
The encrypted repository is protected by a password stored on every node's keystore.
The repository keys (KEK - key encryption key) are generated from the password
using the PBKDF2 function, and are used to encrypt (using the AES Wrap algorithm)
other symmetric keys (referred to as DEK - data encryption keys) which are themselves
used to encrypt the blobs of the regular snapshot.

The platinum or enterprise licenses are required to snapshot to the encrypted repository,
but no license is required to list or restore already encrypted snapshots.
  • Loading branch information
albertzaharovits authored Dec 2, 2020
1 parent 3c412e6 commit 3249cc3
Show file tree
Hide file tree
Showing 34 changed files with 3,286 additions and 157 deletions.
15 changes: 15 additions & 0 deletions plugins/repository-azure/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,18 @@ task azureThirdPartyTest(type: Test) {
}
}
tasks.named("check").configure { dependsOn("azureThirdPartyTest") }

// test jar is exported by the integTestArtifacts configuration to be used in the encrypted Azure repository test
configurations {
internalClusterTestArtifacts.extendsFrom internalClusterTestImplementation
internalClusterTestArtifacts.extendsFrom internalClusterTestRuntime
}

def internalClusterTestJar = tasks.register("internalClusterTestJar", Jar) {
appendix 'internalClusterTest'
from sourceSets.internalClusterTest.output
}

artifacts {
internalClusterTestArtifacts internalClusterTestJar
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,15 @@ protected String repositoryType() {
}

@Override
protected Settings repositorySettings() {
return Settings.builder()
.put(super.repositorySettings())
.put(AzureRepository.Repository.CONTAINER_SETTING.getKey(), "container")
.put(AzureStorageSettings.ACCOUNT_SETTING.getKey(), "test")
.build();
protected Settings repositorySettings(String repoName) {
Settings.Builder settingsBuilder = Settings.builder()
.put(super.repositorySettings(repoName))
.put(AzureRepository.Repository.CONTAINER_SETTING.getKey(), "container")
.put(AzureStorageSettings.ACCOUNT_SETTING.getKey(), "test");
if (randomBoolean()) {
settingsBuilder.put(AzureRepository.Repository.BASE_PATH_SETTING.getKey(), randomFrom("test", "test/1"));
}
return settingsBuilder.build();
}

@Override
Expand Down
19 changes: 18 additions & 1 deletion plugins/repository-gcs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,21 @@ def gcsThirdPartyTest = tasks.register("gcsThirdPartyTest", Test) {
tasks.named("check").configure {
dependsOn(largeBlobYamlRestTest, gcsThirdPartyTest)
}
}
// test jar is exported by the integTestArtifacts configuration to be used in the encrypted GCS repository test
configurations {
internalClusterTestArtifacts.extendsFrom internalClusterTestImplementation
internalClusterTestArtifacts.extendsFrom internalClusterTestRuntime
}
def internalClusterTestJar = tasks.register("internalClusterTestJar", Jar) {
appendix 'internalClusterTest'
from sourceSets.internalClusterTest.output
// for the repositories.gcs.TestUtils class
from sourceSets.test.output
}
artifacts {
internalClusterTestArtifacts internalClusterTestJar
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.CREDENTIALS_FILE_SETTING;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.ENDPOINT_SETTING;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.TOKEN_URI_SETTING;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BASE_PATH;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BUCKET;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.CLIENT_NAME;

Expand All @@ -79,12 +80,15 @@ protected String repositoryType() {
}

@Override
protected Settings repositorySettings() {
return Settings.builder()
.put(super.repositorySettings())
.put(BUCKET.getKey(), "bucket")
.put(CLIENT_NAME.getKey(), "test")
.build();
protected Settings repositorySettings(String repoName) {
Settings.Builder settingsBuilder = Settings.builder()
.put(super.repositorySettings(repoName))
.put(BUCKET.getKey(), "bucket")
.put(CLIENT_NAME.getKey(), "test");
if (randomBoolean()) {
settingsBuilder.put(BASE_PATH.getKey(), randomFrom("test", "test/1"));
}
return settingsBuilder.build();
}

@Override
Expand Down Expand Up @@ -120,7 +124,7 @@ protected Settings nodeSettings(int nodeOrdinal) {
}

public void testDeleteSingleItem() {
final String repoName = createRepository(randomName());
final String repoName = createRepository(randomRepositoryName());
final RepositoriesService repositoriesService = internalCluster().getMasterNodeInstance(RepositoriesService.class);
final BlobStoreRepository repository = (BlobStoreRepository) repositoriesService.repository(repoName);
PlainActionFuture.get(f -> repository.threadPool().generic().execute(ActionRunnable.run(f, () ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protected String repositoryType() {
}

@Override
protected Settings repositorySettings() {
protected Settings repositorySettings(String repoName) {
return Settings.builder()
.put("uri", "hdfs:///")
.put("conf.fs.AbstractFileSystem.hdfs.impl", TestingFs.class.getName())
Expand All @@ -47,6 +47,12 @@ protected Settings repositorySettings() {
.put("compress", randomBoolean()).build();
}

@Override
public void testSnapshotAndRestore() throws Exception {
// the HDFS mockup doesn't preserve the repository contents after removing the repository
testSnapshotAndRestore(false);
}

@Override
protected Collection<Class<? extends Plugin>> nodePlugins() {
return Collections.singletonList(HdfsPlugin.class);
Expand Down
19 changes: 18 additions & 1 deletion plugins/repository-s3/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,21 @@ tasks.named("thirdPartyAudit").configure {
'com.amazonaws.services.kms.model.GenerateDataKeyResult',
'javax.activation.DataHandler'
)
}
}
// test jar is exported by the integTestArtifacts configuration to be used in the encrypted S3 repository test
configurations {
internalClusterTestArtifacts.extendsFrom internalClusterTestImplementation
internalClusterTestArtifacts.extendsFrom internalClusterTestRuntime
}
def internalClusterTestJar = tasks.register("internalClusterTestJar", Jar) {
appendix 'internalClusterTest'
from sourceSets.internalClusterTest.output
// for the plugin-security.policy resource
from sourceSets.test.output
}
artifacts {
internalClusterTestArtifacts internalClusterTestJar
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,17 @@ protected String repositoryType() {
}

@Override
protected Settings repositorySettings() {
return Settings.builder()
.put(super.repositorySettings())
protected Settings repositorySettings(String repoName) {
Settings.Builder settingsBuilder = Settings.builder()
.put(super.repositorySettings(repoName))
.put(S3Repository.BUCKET_SETTING.getKey(), "bucket")
.put(S3Repository.CLIENT_NAME.getKey(), "test")
// Don't cache repository data because some tests manually modify the repository data
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
.build();
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false);
if (randomBoolean()) {
settingsBuilder.put(S3Repository.BASE_PATH_SETTING.getKey(), randomFrom("test", "test/1"));
}
return settingsBuilder.build();
}

@Override
Expand Down Expand Up @@ -146,8 +149,9 @@ protected Settings nodeSettings(int nodeOrdinal) {
}

public void testEnforcedCooldownPeriod() throws IOException {
final String repoName = createRepository(randomName(), Settings.builder().put(repositorySettings())
.put(S3Repository.COOLDOWN_PERIOD.getKey(), TEST_COOLDOWN_PERIOD).build());
final String repoName = randomRepositoryName();
createRepository(repoName, Settings.builder().put(repositorySettings(repoName))
.put(S3Repository.COOLDOWN_PERIOD.getKey(), TEST_COOLDOWN_PERIOD).build(), true);

final SnapshotId fakeOldSnapshot = client().admin().cluster().prepareCreateSnapshot(repoName, "snapshot-old")
.setWaitForCompletion(true).setIndices().get().getSnapshotInfo().snapshotId();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.repositories.fs;

import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.repositories.blobstore.ESFsBasedRepositoryIntegTestCase;

public class FsBlobStoreRepositoryIntegTests extends ESFsBasedRepositoryIntegTestCase {

@Override
protected Settings repositorySettings(String repositoryName) {
final Settings.Builder settings = Settings.builder()
.put("compress", randomBoolean())
.put("location", randomRepoPath());
if (randomBoolean()) {
long size = 1 << randomInt(10);
settings.put("chunk_size", new ByteSizeValue(size, ByteSizeUnit.KB));
}
return settings.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

/**
* The list of paths where a blob can reside. The contents of the paths are dependent upon the implementation of {@link BlobContainer}.
Expand Down Expand Up @@ -90,4 +91,17 @@ public String toString() {
}
return sb.toString();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlobPath other = (BlobPath) o;
return paths.equals(other.paths);
}

@Override
public int hashCode() {
return Objects.hash(paths);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.elasticsearch.repositories.RepositoriesService;
import org.elasticsearch.repositories.Repository;
import org.elasticsearch.repositories.RepositoryData;
import org.elasticsearch.repositories.RepositoryMissingException;
import org.elasticsearch.snapshots.SnapshotMissingException;
import org.elasticsearch.snapshots.SnapshotRestoreException;
import org.elasticsearch.test.ESIntegTestCase;
Expand Down Expand Up @@ -78,17 +79,19 @@ public static RepositoryData getRepositoryData(Repository repository) {

protected abstract String repositoryType();

protected Settings repositorySettings() {
protected Settings repositorySettings(String repoName) {
return Settings.builder().put("compress", randomBoolean()).build();
}

protected final String createRepository(final String name) {
return createRepository(name, repositorySettings());
return createRepository(name, true);
}

protected final String createRepository(final String name, final Settings settings) {
final boolean verify = randomBoolean();
protected final String createRepository(final String name, final boolean verify) {
return createRepository(name, repositorySettings(name), verify);
}

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

return name;
}

protected final void deleteRepository(final String name) {
logger.debug("--> deleting repository [name: {}]", name);
assertAcked(client().admin().cluster().prepareDeleteRepository(name));
internalCluster().getDataOrMasterNodeInstances(RepositoriesService.class).forEach(repositories -> {
RepositoryMissingException e = expectThrows(RepositoryMissingException.class, () -> repositories.repository(name));
assertThat(e.repository(), equalTo(name));
});
}

public void testReadNonExistingPath() throws IOException {
try (BlobStore store = newBlobStore()) {
final BlobContainer container = store.blobContainer(new BlobPath());
Expand Down Expand Up @@ -176,7 +188,7 @@ public void testList() throws IOException {
BlobMetadata blobMetadata = blobs.get(generated.getKey());
assertThat(generated.getKey(), blobMetadata, CoreMatchers.notNullValue());
assertThat(blobMetadata.name(), CoreMatchers.equalTo(generated.getKey()));
assertThat(blobMetadata.length(), CoreMatchers.equalTo(generated.getValue()));
assertThat(blobMetadata.length(), CoreMatchers.equalTo(blobLengthFromContentLength(generated.getValue())));
}

assertThat(container.listBlobsByPrefix("foo-").size(), CoreMatchers.equalTo(numberOfFooBlobs));
Expand Down Expand Up @@ -263,15 +275,25 @@ protected static void writeBlob(BlobContainer container, String blobName, BytesA
}

protected BlobStore newBlobStore() {
final String repository = createRepository(randomName());
final String repository = createRepository(randomRepositoryName());
return newBlobStore(repository);
}

protected BlobStore newBlobStore(String repository) {
final BlobStoreRepository blobStoreRepository =
(BlobStoreRepository) internalCluster().getMasterNodeInstance(RepositoriesService.class).repository(repository);
return PlainActionFuture.get(
f -> blobStoreRepository.threadPool().generic().execute(ActionRunnable.supply(f, blobStoreRepository::blobStore)));
}

public void testSnapshotAndRestore() throws Exception {
final String repoName = createRepository(randomName());
testSnapshotAndRestore(randomBoolean());
}

protected void testSnapshotAndRestore(boolean recreateRepositoryBeforeRestore) throws Exception {
final String repoName = randomRepositoryName();
final Settings repoSettings = repositorySettings(repoName);
createRepository(repoName, repoSettings, randomBoolean());
int indexCount = randomIntBetween(1, 5);
int[] docCounts = new int[indexCount];
String[] indexNames = generateRandomNames(indexCount);
Expand Down Expand Up @@ -319,6 +341,11 @@ public void testSnapshotAndRestore() throws Exception {
assertAcked(client().admin().indices().prepareClose(closeIndices.toArray(new String[closeIndices.size()])));
}

if (recreateRepositoryBeforeRestore) {
deleteRepository(repoName);
createRepository(repoName, repoSettings, randomBoolean());
}

logger.info("--> restore all indices from the snapshot");
assertSuccessfulRestore(client().admin().cluster().prepareRestoreSnapshot(repoName, snapshotName).setWaitForCompletion(true));

Expand All @@ -343,7 +370,7 @@ public void testSnapshotAndRestore() throws Exception {
}

public void testMultipleSnapshotAndRollback() throws Exception {
final String repoName = createRepository(randomName());
final String repoName = createRepository(randomRepositoryName());
int iterationCount = randomIntBetween(2, 5);
int[] docCounts = new int[iterationCount];
String indexName = randomName();
Expand Down Expand Up @@ -398,7 +425,7 @@ public void testMultipleSnapshotAndRollback() throws Exception {
}

public void testIndicesDeletedFromRepository() throws Exception {
final String repoName = createRepository("test-repo");
final String repoName = createRepository(randomRepositoryName());
Client client = client();
createIndex("test-idx-1", "test-idx-2", "test-idx-3");
ensureGreen();
Expand Down Expand Up @@ -495,7 +522,15 @@ private static void assertSuccessfulRestore(RestoreSnapshotResponse response) {
assertThat(response.getRestoreInfo().successfulShards(), equalTo(response.getRestoreInfo().totalShards()));
}

protected static String randomName() {
protected String randomName() {
return randomAlphaOfLength(randomIntBetween(1, 10)).toLowerCase(Locale.ROOT);
}

protected String randomRepositoryName() {
return randomName();
}

protected long blobLengthFromContentLength(long contentLength) {
return contentLength;
}
}
Loading

0 comments on commit 3249cc3

Please sign in to comment.