NIFI-9221: Add AWS SecretsManager Sensitive Props Provider

This closes #5410

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Joe Gresock 2021-09-22 13:18:43 -04:00 committed by exceptionfactory
parent 70432507df
commit f3136f07eb
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
20 changed files with 661 additions and 97 deletions

View File

@ -34,7 +34,7 @@ public class BootstrapProperties extends StandardReadableProperties {
public enum BootstrapPropertyKey {
SENSITIVE_KEY("bootstrap.sensitive.key"),
HASHICORP_VAULT_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.hashicorp.vault.conf"),
AWS_KMS_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.aws.kms.conf"),
AWS_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.aws.conf"),
AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.azure.keyvault.conf"),
GCP_KMS_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.gcp.kms.conf"),
CONTEXT_MAPPING_PREFIX("bootstrap.protection.context.mapping.");

View File

@ -64,4 +64,20 @@ public class ProtectedPropertyContext {
public String getContextKey() {
return String.format("%s/%s", contextName, propertyName);
}
/**
* Returns the property name
* @return The property name
*/
public String getPropertyName() {
return propertyName;
}
/**
* Returns the context name
* @return The context name
*/
public String getContextName() {
return contextName;
}
}

View File

@ -71,6 +71,21 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>secretsmanager</artifactId>
<version>${aws.sdk.version}</version>
<exclusions>
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
</exclusion>
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-secrets</artifactId>

View File

@ -43,7 +43,7 @@ public abstract class AbstractHashiCorpVaultSensitivePropertyProvider extends Ab
if (hasRequiredVaultProperties()) {
try {
vaultCommunicationService = new StandardHashiCorpVaultCommunicationService(getVaultPropertySource(vaultBootstrapConfFilename));
} catch (IOException e) {
} catch (final IOException e) {
throw new SensitivePropertyProtectionException("Error configuring HashiCorpVaultCommunicationService", e);
}
} else {
@ -64,7 +64,7 @@ public abstract class AbstractHashiCorpVaultSensitivePropertyProvider extends Ab
try {
vaultBootstrapProperties = AbstractBootstrapPropertiesLoader.loadBootstrapProperties(
Paths.get(vaultBootstrapConfFilename), VAULT_PREFIX);
} catch (IOException e) {
} catch (final IOException e) {
throw new SensitivePropertyProtectionException("Could not load " + vaultBootstrapConfFilename, e);
}
} else {
@ -97,14 +97,6 @@ public abstract class AbstractHashiCorpVaultSensitivePropertyProvider extends Ab
return hasRequiredVaultProperties();
}
/**
* Returns the Vault-specific bootstrap properties (e.g., bootstrap-vault.properties)
* @return The Vault-specific bootstrap properties
*/
protected BootstrapProperties getVaultBootstrapProperties() {
return vaultBootstrapProperties;
}
private boolean hasRequiredVaultProperties() {
return vaultBootstrapProperties != null
&& (vaultBootstrapProperties.getProperty(VaultConfigurationKey.URI.getKey()) != null)

View File

@ -0,0 +1,163 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.nifi.properties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class AwsSecretsManagerSensitivePropertyProvider extends AbstractSensitivePropertyProvider {
private final SecretsManagerClient client;
private final ObjectMapper objectMapper;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
AwsSecretsManagerSensitivePropertyProvider(final SecretsManagerClient client) {
super(null);
this.client = client;
this.objectMapper = new ObjectMapper();
}
@Override
public boolean isSupported() {
return client != null;
}
@Override
public String protect(final String unprotectedValue, final ProtectedPropertyContext context)
throws SensitivePropertyProtectionException {
Objects.requireNonNull(context, "Property context must be provided");
Objects.requireNonNull(unprotectedValue, "Property value must be provided");
if (client == null) {
throw new SensitivePropertyProtectionException("AWS Secrets Manager Provider Not Configured");
}
try {
writeLock.lock();
final String secretName = context.getContextName();
final Optional<ObjectNode> secretKeyValuesOptional = getSecretKeyValues(context);
final ObjectNode secretObject = secretKeyValuesOptional.orElse(objectMapper.createObjectNode());
secretObject.put(context.getPropertyName(), unprotectedValue);
final String secretString = objectMapper.writeValueAsString(secretObject);
if (secretKeyValuesOptional.isPresent()) {
client.putSecretValue(builder -> builder.secretId(secretName).secretString(secretString));
} else {
client.createSecret(builder -> builder.name(secretName).secretString(secretString));
}
return context.getContextKey();
} catch (final SecretsManagerException | JsonProcessingException e) {
throw new SensitivePropertyProtectionException(String.format("AWS Secrets Manager Secret Could Not Be Stored for [%s]", context), e);
} finally {
writeLock.unlock();
}
}
@Override
public String unprotect(final String protectedValue, final ProtectedPropertyContext context)
throws SensitivePropertyProtectionException {
Objects.requireNonNull(context, "Property context must be provided");
if (client == null) {
throw new SensitivePropertyProtectionException("AWS Secrets Manager Provider Not Configured");
}
try {
readLock.lock();
String propertyValue = null;
final Optional<ObjectNode> secretKeyValuesOptional = getSecretKeyValues(context);
if (secretKeyValuesOptional.isPresent()) {
final ObjectNode secretKeyValues = secretKeyValuesOptional.get();
final String propertyName = context.getPropertyName();
if (secretKeyValues.has(propertyName)) {
propertyValue = secretKeyValues.get(propertyName).textValue();
}
}
if (propertyValue == null) {
throw new SensitivePropertyProtectionException(
String.format("AWS Secret Name [%s] Property Name [%s] not found", context.getContextName(), context.getPropertyName()));
}
return propertyValue;
} finally {
readLock.unlock();
}
}
/**
* Returns the optional parsed JSON from the matching secret, or empty if the secret does not exist.
* @param context The property context
* @return The optional parsed JSON, or empty if the secret does not exist
*/
private Optional<ObjectNode> getSecretKeyValues(final ProtectedPropertyContext context) {
try {
final GetSecretValueResponse response = client.getSecretValue(builder -> builder.secretId(context.getContextName()));
if (response.secretString() == null) {
throw new SensitivePropertyProtectionException(String.format("AWS Secret Name [%s] string value not found",
context.getContextKey()));
}
final JsonNode responseNode = objectMapper.readTree(response.secretString());
if (!(responseNode instanceof ObjectNode)) {
throw new SensitivePropertyProtectionException(String.format("AWS Secrets Manager Secret [%s] JSON parsing failed",
context.getContextKey()));
}
return Optional.of((ObjectNode) responseNode) ;
} catch (final ResourceNotFoundException e) {
return Optional.empty();
} catch (final SecretsManagerException e) {
throw new SensitivePropertyProtectionException(String.format("AWS Secrets Manager Secret [%s] retrieval failed",
context.getContextKey()), e);
} catch (final JsonProcessingException e) {
throw new SensitivePropertyProtectionException(String.format("AWS Secrets Manager Secret [%s] JSON parsing failed",
context.getContextKey()), e);
}
}
@Override
protected PropertyProtectionScheme getProtectionScheme() {
return PropertyProtectionScheme.AWS_SECRETSMANAGER;
}
@Override
public String getIdentifierKey() {
return getProtectionScheme().getIdentifier();
}
@Override
public void cleanUp() {
if (client != null) {
client.close();
}
}
}

View File

@ -25,6 +25,7 @@ import java.util.Objects;
*/
public enum PropertyProtectionScheme {
AES_GCM("aes/gcm/(128|192|256)", "aes/gcm/%s", "AES Sensitive Property Provider", true),
AWS_SECRETSMANAGER("aws/secretsmanager", "aws/secretsmanager", "AWS Secrets Manager Sensitive Property Provider", false),
AWS_KMS("aws/kms", "aws/kms", "AWS KMS Sensitive Property Provider", false),
AZURE_KEYVAULT_KEY("azure/keyvault/key", "azure/keyvault/key", "Azure Key Vault Key Sensitive Property Provider", false),
AZURE_KEYVAULT_SECRET("azure/keyvault/secret", "azure/keyvault/secret", "Azure Key Vault Secret Sensitive Property Provider", false),

View File

@ -21,6 +21,7 @@ import com.azure.security.keyvault.secrets.SecretClient;
import com.google.cloud.kms.v1.KeyManagementServiceClient;
import org.apache.nifi.properties.BootstrapProperties.BootstrapPropertyKey;
import org.apache.nifi.properties.configuration.AwsKmsClientProvider;
import org.apache.nifi.properties.configuration.AwsSecretsManagerClientProvider;
import org.apache.nifi.properties.configuration.AzureCryptographyClientProvider;
import org.apache.nifi.properties.configuration.AzureSecretClientProvider;
import org.apache.nifi.properties.configuration.ClientProvider;
@ -30,6 +31,7 @@ import org.apache.nifi.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import java.io.IOException;
import java.util.Arrays;
@ -137,6 +139,13 @@ public class StandardSensitivePropertyProviderFactory implements SensitiveProper
final Optional<KmsClient> kmsClient = clientProvider.getClient(clientProperties);
return new AwsKmsSensitivePropertyProvider(kmsClient.orElse(null), clientProperties);
});
case AWS_SECRETSMANAGER:
return providerMap.computeIfAbsent(protectionScheme, s -> {
final AwsSecretsManagerClientProvider clientProvider = new AwsSecretsManagerClientProvider();
final Properties clientProperties = getClientProperties(clientProvider);
final Optional<SecretsManagerClient> secretsManagerClient = clientProvider.getClient(clientProperties);
return new AwsSecretsManagerSensitivePropertyProvider(secretsManagerClient.orElse(null));
});
case AZURE_KEYVAULT_KEY:
return providerMap.computeIfAbsent(protectionScheme, s -> {
final AzureCryptographyClientProvider clientProvider = new AzureCryptographyClientProvider();

View File

@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.nifi.properties.configuration;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.properties.BootstrapProperties;
import org.apache.nifi.properties.SensitivePropertyProtectionException;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.core.SdkClient;
import java.util.Properties;
/**
* Amazon Web Services Service Client Provider base class
*/
public abstract class AbstractAwsClientProvider<T extends SdkClient> extends BootstrapPropertiesClientProvider<T> {
private static final String ACCESS_KEY_PROPS_NAME = "aws.access.key.id";
private static final String SECRET_KEY_PROPS_NAME = "aws.secret.access.key";
private static final String REGION_KEY_PROPS_NAME = "aws.region";
public AbstractAwsClientProvider() {
super(BootstrapProperties.BootstrapPropertyKey.AWS_SENSITIVE_PROPERTY_PROVIDER_CONF);
}
/**
* Get Configured Client using either Client Properties or AWS Default Credentials Provider
*
* @param clientProperties Client Properties
* @return KMS Client
*/
@Override
protected T getConfiguredClient(final Properties clientProperties) {
final String accessKey = clientProperties.getProperty(ACCESS_KEY_PROPS_NAME);
final String secretKey = clientProperties.getProperty(SECRET_KEY_PROPS_NAME);
final String region = clientProperties.getProperty(REGION_KEY_PROPS_NAME);
if (StringUtils.isNoneBlank(accessKey, secretKey, region)) {
logger.debug("AWS Credentials Location: Client Properties");
try {
final AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
return createClient(credentials, region);
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("AWS Client Builder Failed using Client Properties", e);
}
} else {
logger.debug("AWS Credentials Location: Default Credentials Provider");
try {
final DefaultCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build();
return createDefaultClient(credentialsProvider);
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("AWS Client Builder Failed using Default Credentials Provider", e);
}
}
}
/**
* Create a client with the given credentials and region.
* @param credentials AWS credentials
* @param region AWS region
* @return The created client
*/
protected abstract T createClient(AwsCredentials credentials, String region);
/**
* Create a default client with the given credentials provider.
* @param credentialsProvider AWS credentials provider
* @return The created client
*/
protected abstract T createDefaultClient(AwsCredentialsProvider credentialsProvider);
}

View File

@ -16,65 +16,37 @@
*/
package org.apache.nifi.properties.configuration;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.properties.BootstrapProperties;
import org.apache.nifi.properties.SensitivePropertyProtectionException;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.KmsClientBuilder;
import java.util.Properties;
import java.util.Collections;
import java.util.Set;
/**
* Amazon Web Services Key Management Service Client Provider
*/
public class AwsKmsClientProvider extends BootstrapPropertiesClientProvider<KmsClient> {
private static final String ACCESS_KEY_PROPS_NAME = "aws.access.key.id";
public class AwsKmsClientProvider extends AbstractAwsClientProvider<KmsClient> {
private static final String SECRET_KEY_PROPS_NAME = "aws.secret.access.key";
protected static final String KEY_ID_PROPERTY = "aws.kms.key.id";
private static final String REGION_KEY_PROPS_NAME = "aws.region";
public AwsKmsClientProvider() {
super(BootstrapProperties.BootstrapPropertyKey.AWS_KMS_SENSITIVE_PROPERTY_PROVIDER_CONF);
@Override
protected KmsClient createClient(final AwsCredentials credentials, final String region) {
return KmsClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of(region))
.build();
}
/**
* Get Configured Client using either Client Properties or AWS Default Credentials Provider
*
* @param clientProperties Client Properties
* @return KMS Client
*/
@Override
protected KmsClient getConfiguredClient(final Properties clientProperties) {
final String accessKey = clientProperties.getProperty(ACCESS_KEY_PROPS_NAME);
final String secretKey = clientProperties.getProperty(SECRET_KEY_PROPS_NAME);
final String region = clientProperties.getProperty(REGION_KEY_PROPS_NAME);
protected KmsClient createDefaultClient(final AwsCredentialsProvider credentialsProvider) {
return KmsClient.builder().credentialsProvider(credentialsProvider).build();
}
final KmsClientBuilder kmsClientBuilder = KmsClient.builder();
if (StringUtils.isNoneBlank(accessKey, secretKey, region)) {
logger.debug("AWS Credentials Location: Client Properties");
try {
final AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
return kmsClientBuilder
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("AWS KMS Client Builder Failed using Client Properties", e);
}
} else {
logger.debug("AWS Credentials Location: Default Credentials Provider");
try {
final DefaultCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build();
return kmsClientBuilder.credentialsProvider(credentialsProvider).build();
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("AWS KMS Client Builder Failed using Default Credentials Provider", e);
}
}
@Override
protected Set<String> getRequiredPropertyNames() {
return Collections.singleton(KEY_ID_PROPERTY);
}
}

View File

@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.nifi.properties.configuration;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import java.util.Collections;
import java.util.Set;
/**
* Amazon Web Services Secrets Manager Client Provider
*/
public class AwsSecretsManagerClientProvider extends AbstractAwsClientProvider<SecretsManagerClient> {
@Override
protected SecretsManagerClient createClient(final AwsCredentials credentials, final String region) {
return SecretsManagerClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of(region))
.build();
}
@Override
protected SecretsManagerClient createDefaultClient(final AwsCredentialsProvider credentialsProvider) {
return SecretsManagerClient.builder().credentialsProvider(credentialsProvider).build();
}
@Override
protected Set<String> getRequiredPropertyNames() {
return Collections.emptySet();
}
}

View File

@ -19,11 +19,6 @@ package org.apache.nifi.properties.configuration;
import com.azure.core.credential.TokenCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import org.apache.nifi.properties.BootstrapProperties;
import org.apache.nifi.util.StringUtils;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
/**
* Abstract Microsoft Azure Client Provider
@ -33,17 +28,6 @@ public abstract class AzureClientProvider<T> extends BootstrapPropertiesClientPr
super(BootstrapProperties.BootstrapPropertyKey.AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF);
}
/**
* Get Client using Client Properties
*
* @param clientProperties Client Properties can be null
* @return Configured Client or empty when Client Properties object is null
*/
@Override
public Optional<T> getClient(final Properties clientProperties) {
return isMissingProperties(clientProperties) ? Optional.empty() : Optional.of(getConfiguredClient(clientProperties));
}
/**
* Get Default Azure Token Credential using Default Credentials Builder for environment variables and system properties
*
@ -52,17 +36,4 @@ public abstract class AzureClientProvider<T> extends BootstrapPropertiesClientPr
protected TokenCredential getDefaultTokenCredential() {
return new DefaultAzureCredentialBuilder().build();
}
/**
* Get Property Names required for initializing client in order to perform initial validation
*
* @return Set of required client property names
*/
protected abstract Set<String> getRequiredPropertyNames();
private boolean isMissingProperties(final Properties clientProperties) {
return clientProperties == null || getRequiredPropertyNames().stream().anyMatch(propertyName ->
StringUtils.isBlank(clientProperties.getProperty(propertyName))
);
}
}

View File

@ -30,6 +30,7 @@ import java.nio.file.Paths;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
/**
* Shared Client Provider for reading Client Properties from file referenced in configured Bootstrap Property Key
@ -53,7 +54,7 @@ public abstract class BootstrapPropertiesClientProvider<T> implements ClientProv
*/
@Override
public Optional<T> getClient(final Properties clientProperties) {
return clientProperties == null ? Optional.empty() : Optional.of(getConfiguredClient(clientProperties));
return isMissingProperties(clientProperties) ? Optional.empty() : Optional.of(getConfiguredClient(clientProperties));
}
/**
@ -96,4 +97,17 @@ public abstract class BootstrapPropertiesClientProvider<T> implements ClientProv
* @return Configured Client
*/
protected abstract T getConfiguredClient(final Properties clientProperties);
/**
* Get Property Names required for initializing client in order to perform initial validation
*
* @return Set of required client property names
*/
protected abstract Set<String> getRequiredPropertyNames();
private boolean isMissingProperties(final Properties clientProperties) {
return clientProperties == null || getRequiredPropertyNames().stream().anyMatch(propertyName ->
StringUtils.isBlank(clientProperties.getProperty(propertyName))
);
}
}

View File

@ -22,7 +22,9 @@ import org.apache.nifi.properties.BootstrapProperties;
import org.apache.nifi.properties.SensitivePropertyProtectionException;
import java.io.IOException;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;
/**
* Google Key Management Service Client Provider
@ -46,4 +48,9 @@ public class GoogleKeyManagementServiceClientProvider extends BootstrapPropertie
throw new SensitivePropertyProtectionException("Google Key Management Service Create Failed", e);
}
}
@Override
protected Set<String> getRequiredPropertyNames() {
return Collections.emptySet();
}
}

View File

@ -61,7 +61,7 @@ public class AwsKmsSensitivePropertyProviderIT {
private static final String REGION_KEY_PROPS_NAME = "aws.region";
private static final String KMS_KEY_PROPS_NAME = "aws.kms.key.id";
private static final String BOOTSTRAP_AWS_FILE_PROPS_NAME = "nifi.bootstrap.protection.aws.kms.conf";
private static final String BOOTSTRAP_AWS_FILE_PROPS_NAME = "nifi.bootstrap.protection.aws.conf";
private static final String EMPTY_PROPERTY = "";

View File

@ -0,0 +1,130 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.nifi.properties;
import org.apache.nifi.properties.configuration.AwsSecretsManagerClientProvider;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.internal.util.io.IOUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
/**
* To run this test, make sure to first configure sensitive credential information as in the following link
* https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html
*
* The following settings are optional. If you have a default AWS configuration and credentials in ~/.aws then
* it will take that. Otherwise you can set all of the following:
* set the system property -Daws.access.key.id to the access key id
* set the system property -Daws.secret.access.key to the secret access key
* set the system property -Daws.region to the region
*
* After you are satisfied with the test, and you don't need the key, you may schedule secret deletion with:
* aws secretsmanager delete-secret --secret-id "default/property" --recovery-window-in-days 14
*
*/
public class AwsSecretsManagerSensitivePropertyProviderIT {
private static final String SAMPLE_PLAINTEXT = "AWSSecretsManagerSensitivePropertyProviderIT SAMPLE-PLAINTEXT";
private static final String ACCESS_KEY_PROPS_NAME = "aws.access.key.id";
private static final String SECRET_KEY_PROPS_NAME = "aws.secret.access.key";
private static final String REGION_KEY_PROPS_NAME = "aws.region";
private static final String BOOTSTRAP_AWS_FILE_PROPS_NAME = "nifi.bootstrap.protection.aws.conf";
private static final String EMPTY_PROPERTY = "";
private static AwsSecretsManagerSensitivePropertyProvider spp;
private static BootstrapProperties props;
private static Path mockBootstrapConf, mockAWSBootstrapConf;
private static final Logger logger = LoggerFactory.getLogger(AwsSecretsManagerSensitivePropertyProviderIT.class);
private static void initializeBootstrapProperties() throws IOException{
mockBootstrapConf = Files.createTempFile("bootstrap", ".conf").toAbsolutePath();
mockAWSBootstrapConf = Files.createTempFile("bootstrap-aws", ".conf").toAbsolutePath();
IOUtil.writeText(BOOTSTRAP_AWS_FILE_PROPS_NAME + "=" + mockAWSBootstrapConf.toAbsolutePath(), mockBootstrapConf.toFile());
final Properties bootstrapProperties = new Properties();
try (final InputStream inputStream = Files.newInputStream(mockBootstrapConf)) {
bootstrapProperties.load(inputStream);
props = new BootstrapProperties("nifi", bootstrapProperties, mockBootstrapConf);
}
String accessKey = System.getProperty(ACCESS_KEY_PROPS_NAME, EMPTY_PROPERTY);
String secretKey = System.getProperty(SECRET_KEY_PROPS_NAME, EMPTY_PROPERTY);
String region = System.getProperty(REGION_KEY_PROPS_NAME, EMPTY_PROPERTY);
StringBuilder bootstrapConfText = new StringBuilder();
String lineSeparator = System.getProperty("line.separator");
bootstrapConfText.append(ACCESS_KEY_PROPS_NAME + "=" + accessKey);
bootstrapConfText.append(lineSeparator + SECRET_KEY_PROPS_NAME + "=" + secretKey);
bootstrapConfText.append(lineSeparator + REGION_KEY_PROPS_NAME + "=" + region);
IOUtil.writeText(bootstrapConfText.toString(), mockAWSBootstrapConf.toFile());
}
@BeforeClass
public static void initOnce() throws IOException {
initializeBootstrapProperties();
Assert.assertNotNull(props);
final AwsSecretsManagerClientProvider provider = new AwsSecretsManagerClientProvider();
final Properties properties = provider.getClientProperties(props).orElse(null);
final SecretsManagerClient secretsManagerClient = provider.getClient(properties).orElse(null);
spp = new AwsSecretsManagerSensitivePropertyProvider(secretsManagerClient);
Assert.assertNotNull(spp);
}
@AfterClass
public static void tearDownOnce() throws IOException {
Files.deleteIfExists(mockBootstrapConf);
Files.deleteIfExists(mockAWSBootstrapConf);
spp.cleanUp();
}
@Test
public void testEncryptDecrypt() {
logger.info("Running testEncryptDecrypt of AWS Secrets Manager SPP integration test");
runEncryptDecryptTest();
logger.info("testEncryptDecrypt of AWS Secrets Manager SPP integration test completed");
}
private static void runEncryptDecryptTest() {
final String propertyName = "property2";
logger.info("Plaintext: " + SAMPLE_PLAINTEXT);
String protectedValue = spp.protect(SAMPLE_PLAINTEXT, ProtectedPropertyContext.defaultContext(propertyName));
logger.info("Protected Value: " + protectedValue);
String unprotectedValue = spp.unprotect(protectedValue, ProtectedPropertyContext.defaultContext(propertyName));
logger.info("Unprotected Value: " + unprotectedValue);
Assert.assertEquals(SAMPLE_PLAINTEXT, unprotectedValue);
Assert.assertNotEquals(SAMPLE_PLAINTEXT, protectedValue);
Assert.assertNotEquals(protectedValue, unprotectedValue);
}
}

View File

@ -0,0 +1,116 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.nifi.properties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretResponse;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
import software.amazon.awssdk.services.secretsmanager.model.PutSecretValueResponse;
import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
import java.nio.charset.Charset;
import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class AwsSecretsManagerSensitivePropertyProviderTest {
private static final String PROPERTY_NAME = "propertyName";
private static final String PROPERTY_VALUE = "propertyValue";
private static final String SECRET_STRING = String.format("{ \"%s\": \"%s\" }", PROPERTY_NAME, PROPERTY_VALUE);
@Mock
private SecretsManagerClient secretsManagerClient;
private AwsSecretsManagerSensitivePropertyProvider provider;
@BeforeEach
public void setProvider() {
provider = new AwsSecretsManagerSensitivePropertyProvider(secretsManagerClient);
}
@Test
public void testValidateClientNull() {
final AwsSecretsManagerSensitivePropertyProvider provider = new AwsSecretsManagerSensitivePropertyProvider(null);
assertNotNull(provider);
}
@Test
public void testValidateKeyNoSecretString() {
final GetSecretValueResponse getSecretValueResponse = GetSecretValueResponse.builder()
.secretBinary(SdkBytes.fromString("binary", Charset.defaultCharset())).build();
when(secretsManagerClient.getSecretValue(any(Consumer.class))).thenReturn(getSecretValueResponse);
assertThrows(SensitivePropertyProtectionException.class, () ->
provider.unprotect("anyValue", ProtectedPropertyContext.defaultContext(PROPERTY_NAME)));
}
@Test
public void testCleanUp() {
provider.cleanUp();
verify(secretsManagerClient).close();
}
@Test
public void testProtectCreateSecret() {
final String secretName = ProtectedPropertyContext.defaultContext(PROPERTY_NAME).getContextKey();
when(secretsManagerClient.getSecretValue(any(Consumer.class))).thenThrow(ResourceNotFoundException.builder().message("Not found").build());
final CreateSecretResponse createSecretResponse = CreateSecretResponse.builder()
.name(secretName).build();
when(secretsManagerClient.createSecret(any(Consumer.class))).thenReturn(createSecretResponse);
final String protectedProperty = provider.protect(PROPERTY_VALUE, ProtectedPropertyContext.defaultContext(PROPERTY_NAME));
assertEquals(secretName, protectedProperty);
}
@Test
public void testProtectExistingSecret() {
final String secretName = ProtectedPropertyContext.defaultContext(PROPERTY_NAME).getContextKey();
final GetSecretValueResponse getSecretValueResponse = GetSecretValueResponse.builder().secretString(SECRET_STRING).build();
when(secretsManagerClient.getSecretValue(any(Consumer.class))).thenReturn(getSecretValueResponse);
final PutSecretValueResponse putSecretValueResponse = PutSecretValueResponse.builder()
.name(secretName).build();
when(secretsManagerClient.putSecretValue(any(Consumer.class))).thenReturn(putSecretValueResponse);
final String protectedProperty = provider.protect(PROPERTY_VALUE, ProtectedPropertyContext.defaultContext(PROPERTY_NAME));
assertEquals(secretName, protectedProperty);
}
@Test
public void testUnprotect() {
final GetSecretValueResponse getSecretValueResponse = GetSecretValueResponse.builder().secretString(SECRET_STRING).build();
when(secretsManagerClient.getSecretValue(any(Consumer.class))).thenReturn(getSecretValueResponse);
final String property = provider.unprotect("anyValue", ProtectedPropertyContext.defaultContext(PROPERTY_NAME));
assertEquals(PROPERTY_VALUE, property);
}
}

View File

@ -1797,6 +1797,7 @@ For more information, see the <<toolkit-guide.adoc#encrypt_config_tool,Encrypt-C
Configuring each Sensitive Property Provider requires including the appropriate file reference property in `bootstrap.conf`. The default `bootstrap.conf` includes commented file reference properties for available providers.
[[hashicorp-vault-providers]]
=== HashiCorp Vault providers
Two encryption providers are currently configurable in the `bootstrap-hashicorp-vault.conf` file:
@ -1838,6 +1839,7 @@ Following are the configuration properties available inside the `bootstrap-hashi
|`vault.ssl.trust-store-password`|Truststore password. Required if the Vault server is TLS-enabled|_none_
|===
[[aws-kms-provider]]
=== AWS KMS provider
This provider uses https://aws.amazon.com/kms/[AWS Key Management Service] for decryption. AWS KMS configuration properties can be stored in the `bootstrap-aws.conf` file, as referenced in `bootstrap.conf`. If the configuration properties are not specified in `bootstrap-aws.conf`, then the provider will attempt to use the AWS default credentials provider, which checks standard environment variables and system properties.
@ -1858,6 +1860,20 @@ This provider uses https://aws.amazon.com/kms/[AWS Key Management Service] for d
|`aws.secret.access.key`|The secret access key used to access AWS KMS.|_none_
|===
[[aws-secrets-manager-provider]]
=== AWS Secrets Manager provider
This provider uses https://aws.amazon.com/secrets-manager/[AWS Secrets Manager Service] to store and retrieve AWS Secrets. AWS Secrets Manager configuration properties can be stored in the `bootstrap-aws.conf` file, as referenced in `bootstrap.conf`. If the configuration properties are not specified in `bootstrap-aws.conf`, then the provider will attempt to use the AWS default credentials provider, which checks standard environment variables and system properties.
==== Optional properties
===== All of the following must be configured, or will be ignored entirely.
[options="header,footer"]
|===
|Property Name|Description|Default
|`aws.region`|The AWS region used to configure the AWS Secrets Manager Client.|_none_
|`aws.access.key.id`|The access key ID credential used to access AWS Secrets Manager.|_none_
|`aws.secret.access.key`|The secret access key used to access AWS Secrets Manager.|_none_
|===
[[azure-key-vault-key-provider]]
=== Azure Key Vault Key Provider
This protection scheme uses keys managed by

View File

@ -435,7 +435,7 @@ The following are available options when targeting NiFi:
* `-f`,`--flowXml <file>` The _flow.xml.gz_ file currently protected with old password (will be overwritten unless `-g` is specified)
* `-g`,`--outputFlowXml <file>` The destination _flow.xml.gz_ file containing protected config values (will not modify input _flow.xml.gz_)
* `-b`,`--bootstrapConf <file>` The bootstrap.conf file to persist root key and to optionally provide any configuration for the protection scheme.
* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>] (default is AES_GCM)
* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AWS_SECRETSMANAGER>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>] (default is AES_GCM)
* `-k`,`--key <keyhex>` The raw hexadecimal key to use to encrypt the sensitive properties
* `-e`,`--oldKey <keyhex>` The old raw hexadecimal key to use during key migration
* `-H`,`--oldProtectionScheme <protectionScheme>` The old protection scheme to use during encryption migration (see --protectionScheme for possible values). Default is AES_GCM
@ -456,7 +456,7 @@ The following are available options when targeting NiFi Registry using the `--ni
* `-v`,`--verbose` Sets verbose mode (default false)
* `-p`,`--password <password>` Protect the files using a password-derived key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the password.
* `-k`,`--key <keyhex>` Protect the files using a raw hexadecimal key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the key.
* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>] (default is AES_GCM)
* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AWS_SECRETSMANAGER>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>] (default is AES_GCM)
* `--oldPassword <password>` If the input files are already protected using a password-derived key, this specifies the old password so that the files can be unprotected before re-protecting.
* `--oldKey <keyhex>` If the input files are already protected using a key, this specifies the raw hexadecimal key so that the files can be unprotected before re-protecting.
* `-H`,`--oldProtectionScheme <protectionScheme>`The old protection scheme to use during encryption migration (see --protectionScheme for possible values). Default is AES_GCM.
@ -477,13 +477,16 @@ The protection scheme can be selected during encryption using the `--protectionS
The default protection scheme, `AES-G/CM` simply encrypts sensitive properties and marks their protection as either `aes/gcm/256` or `aes/gcm/256` as appropriate. This protection is all done within NiFi itself.
==== HASHICORP_VAULT_TRANSIT [[HASHICORP_VAULT_TRANSIT]]
This protection scheme uses https://www.vaultproject.io/docs/secrets/transit[HashiCorp Vault Transit Secrets Engine] to outsource encryption to a configured Vault server. All HashiCorp Vault configuration is stored in the `bootstrap-hashicorp-vault.conf` file, as referenced in the `bootstrap.conf` of a NiFi or NiFi Registry instance. Therefore, when using the HASHICORP_VAULT_TRANSIT protection scheme, the `nifi(.registry)?.bootstrap.protection.hashicorp.vault.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_hashicorp_vault_providers,HashiCorp Vault providers>> section in the link:administration-guide.html[NiFi Administration Guide].
This protection scheme uses https://www.vaultproject.io/docs/secrets/transit[HashiCorp Vault Transit Secrets Engine] to outsource encryption to a configured Vault server. All HashiCorp Vault configuration is stored in the `bootstrap-hashicorp-vault.conf` file, as referenced in the `bootstrap.conf` of a NiFi or NiFi Registry instance. Therefore, when using the HASHICORP_VAULT_TRANSIT protection scheme, the `nifi(.registry)?.bootstrap.protection.hashicorp.vault.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#hashicorp-vault-providers,HashiCorp Vault providers>> section in the link:administration-guide.html[NiFi Administration Guide].
==== HASHICORP_VAULT_KV [[HASHICORP_VAULT_KV]]
This protection scheme uses https://www.vaultproject.io/docs/secrets/kv/kv-v1[HashiCorp Vault Key Value Secrets Engine Version 1] to store sensitive values as Vault Secrets. All HashiCorp Vault configuration is stored in the `bootstrap-hashicorp-vault.conf` file, as referenced in the `bootstrap.conf` of a NiFi or NiFi Registry instance. Therefore, when using the HASHICORP_VAULT_KV protection scheme, the `nifi(.registry)?.bootstrap.protection.hashicorp.vault.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_hashicorp_vault_providers,HashiCorp Vault providers>> section in the link:administration-guide.html[NiFi Administration Guide].
This protection scheme uses https://www.vaultproject.io/docs/secrets/kv/kv-v1[HashiCorp Vault Key Value Secrets Engine Version 1] to store sensitive values as Vault Secrets. All HashiCorp Vault configuration is stored in the `bootstrap-hashicorp-vault.conf` file, as referenced in the `bootstrap.conf` of a NiFi or NiFi Registry instance. Therefore, when using the HASHICORP_VAULT_KV protection scheme, the `nifi(.registry)?.bootstrap.protection.hashicorp.vault.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#hashicorp-vault-providers,HashiCorp Vault providers>> section in the link:administration-guide.html[NiFi Administration Guide].
==== AWS_KMS [[AWS_KMS]]
This protection scheme uses https://aws.amazon.com/kms/[AWS Key Management] Service for encryption and decryption. AWS KMS configuration properties can be stored in the `bootstrap-aws.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. If the configuration properties are not specified in `bootstrap-aws.conf`, then the provider will attempt to use the AWS default credentials provider, which checks standard environment variables and system properties. Therefore, when using the AWS_KMS protection scheme, the `nifi(.registry)?.bootstrap.protection.aws.kms.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_aws_kms_provider, AWS KMS provider>> section in the link:administration-guide.html[NiFi Administration Guide].
This protection scheme uses https://aws.amazon.com/kms/[AWS Key Management] Service for encryption and decryption. AWS KMS configuration properties can be stored in the `bootstrap-aws.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. If the configuration properties are not specified in `bootstrap-aws.conf`, then the provider will attempt to use the AWS default credentials provider, which checks standard environment variables and system properties. Therefore, when using the AWS_KMS protection scheme, the `nifi(.registry)?.bootstrap.protection.aws.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#aws-kms-provider, AWS KMS provider>> section in the link:administration-guide.html[NiFi Administration Guide].
==== AWS_SECRETSMANAGER [[AWS_SECRETSMANAGER]]
This protection scheme uses https://aws.amazon.com/secrets-manager/[AWS Secrets Manager] Service to store sensitive values as AWS Secrets. AWS Secrets Manager configuration properties can be stored in the `bootstrap-aws.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. If the configuration properties are not specified in `bootstrap-aws.conf`, then the provider will attempt to use the AWS default credentials provider, which checks standard environment variables and system properties. Therefore, when using the AWS_SECRETS_MANAGER protection scheme, the `nifi(.registry)?.bootstrap.protection.aws.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#aws-secrets-manager-provider, AWS Secrets Manager provider>> section in the link:administration-guide.html[NiFi Administration Guide].
==== Microsoft Azure Key Vault Sensitive Property Providers

View File

@ -63,8 +63,8 @@ nifi.bootstrap.sensitive.key=
# HashiCorp Vault Sensitive Property Providers
#nifi.bootstrap.protection.hashicorp.vault.conf=./conf/bootstrap-hashicorp-vault.conf
# AWS KMS Sensitive Property Providers
#nifi.bootstrap.protection.aws.kms.conf=./conf/bootstrap-aws.conf
# AWS Sensitive Property Providers
#nifi.bootstrap.protection.aws.conf=./conf/bootstrap-aws.conf
# Azure Key Vault Sensitive Property Providers
#nifi.bootstrap.protection.azure.keyvault.conf=./conf/bootstrap-azure.conf

View File

@ -58,8 +58,8 @@ nifi.registry.bootstrap.sensitive.key=
# HashiCorp Vault Sensitive Property Providers
#nifi.registry.bootstrap.protection.hashicorp.vault.conf=./conf/bootstrap-hashicorp-vault.conf
# AWS KMS Sensitive Property Providers
#nifi.registry.bootstrap.protection.aws.kms.conf=./conf/bootstrap-aws.conf
# AWS Sensitive Property Providers
#nifi.registry.bootstrap.protection.aws.conf=./conf/bootstrap-aws.conf
# Azure Key Vault Sensitive Property Providers
#nifi.registry.bootstrap.protection.azure.keyvault.conf=./conf/bootstrap-azure.conf