io.axual.utilities.config.providers.VaultKeyStoreProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vault-keystore-provider Show documentation
Show all versions of vault-keystore-provider Show documentation
Provides an implementation of the Kafka ConfigProvider to retrieve private keys and
certificate chains from HashiCorp Vault and create files for keystores
The newest version!
package io.axual.utilities.config.providers;
/*-
* ========================LICENSE_START=================================
* Keystore Generation Configuration Provider
* %%
* Copyright (C) 2020 Axual B.V.
* %%
* Licensed 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.
* =========================LICENSE_END==================================
*/
import com.bettercloud.vault.response.LogicalResponse;
import org.apache.kafka.common.config.ConfigData;
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.common.config.provider.ConfigProvider;
import org.apache.kafka.common.config.types.Password;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import io.axual.utilities.config.providers.exceptions.VaultConfigurationException;
import io.axual.utilities.config.providers.exceptions.VaultKeyStoreProviderException;
import io.axual.utilities.config.providers.keystore.KeyData;
import io.axual.utilities.config.providers.keystore.KeyStoreCreator;
import io.axual.utilities.config.providers.keystore.KeyStoreData;
public class VaultKeyStoreProvider implements ConfigProvider {
public static final Logger LOG = LoggerFactory.getLogger(VaultKeyStoreProvider.class);
public static final String KEY_ALIAS = "key";
private final KeyStoreCreator creator;
protected Optional vaultHelper;
private String keyNamePrivateKey;
private String keyNameCertificateChain;
private String temporaryStorageDirectory;
private String truststoreLocation;
private Password truststorePassword;
/**
* A constructor for testing
*
* @param creator the KeyStore Creator to use, will be mocked for testing purposes
*/
VaultKeyStoreProvider(KeyStoreCreator creator, VaultHelper vaultHelper) {
super();
this.creator = creator;
this.vaultHelper = Optional.ofNullable(vaultHelper);
}
/**
* A default constructor is required for the ConfigProvider initialization.
*/
public VaultKeyStoreProvider() {
this(KeyStoreCreator.INSTANCE, null);
}
@Override
public void configure(Map configs) {
LOG.debug("Configuring provider");
if (vaultHelper.isPresent()) {
LOG.error("Previous configuration found, will not configure provider");
} else {
VaultKeyStoreProviderConfig providerConfig = new VaultKeyStoreProviderConfig(configs,
true);
vaultHelper = Optional.ofNullable(createVaultHelper(providerConfig));
vaultHelper.ifPresent(VaultHelper::testConnection);
this.keyNamePrivateKey = providerConfig.getPrivateKeyKeyName().orElseThrow(
() -> new VaultKeyStoreProviderException(
"No key name for the private key entry supplied"));
this.keyNameCertificateChain = providerConfig.getCertificateChainKeyName().orElseThrow(
() -> new VaultKeyStoreProviderException(
"No key name for the certificate chain entry supplied"));
this.temporaryStorageDirectory = providerConfig.getTemporaryStorageDirectory()
.orElseThrow(
() -> new VaultKeyStoreProviderException(
"No temporary storage directory supplied"));
if (!Files.exists(Paths.get(this.temporaryStorageDirectory))) {
throw new VaultKeyStoreProviderException(
"Provided temporary storage directory does not exist");
}
this.truststoreLocation = providerConfig.getTrustStoreLocation().orElseThrow(
() -> new VaultKeyStoreProviderException(
"No truststore location supplied"));
this.truststorePassword = providerConfig.getTrustStorePassword().orElseThrow(
() -> new VaultKeyStoreProviderException(
"No truststore password supplied"));
}
}
@Override
public ConfigData get(String path) {
LOG.info("Get keystore data from vault path {}", path);
return get(path, Collections.emptySet());
}
@Override
public ConfigData get(String path, Set providedKeys) {
VaultHelper helper = this.vaultHelper
.orElseThrow(() -> new VaultConfigurationException("Provider is not yet configured"));
Set keys = providedKeys.stream()
.filter(key -> (key != null && !key.trim().isEmpty()))
.collect(Collectors.toSet());
LOG.info("Get keystore data from vault path {} with keys {}", path, keys);
LogicalResponse response = helper.getData(path);
Map retrieved = response.getData();
logMap("Retrieved data", retrieved);
// Extract the keystore date
if (!retrieved.containsKey(keyNamePrivateKey)) {
throw new VaultKeyStoreProviderException(
"Path '" + path + "' does not contain key for private key. Expected: "
+ keyNamePrivateKey);
}
if (!retrieved.containsKey(keyNameCertificateChain)) {
throw new VaultKeyStoreProviderException(
"Path '" + path + "' does not contain key for certificate chain. Expected: "
+ keyNameCertificateChain);
}
final Password password;
// Generate the file, if needed
File target = null;
try {
final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
String cleanPath = path.replaceAll("[^a-zA-Z0-9-_\\.]", "-");
byte[] hash = messageDigest.digest(cleanPath.getBytes(StandardCharsets.UTF_8));
final String prefix = "keystore__" + cleanPath + "__";
password = new Password(bytesToHexString(hash));
try(DirectoryStream directoryStream = Files.newDirectoryStream(Paths.get(this.temporaryStorageDirectory))) {
for (Path tempPath : directoryStream) {
if (tempPath.getFileName().toString().startsWith(prefix)) {
target = tempPath.toFile();
break;
}
}
}
if (target == null) {
target = File.createTempFile(prefix, "_generated.jks",
new File(this.temporaryStorageDirectory));
}
} catch (IOException e) {
throw new VaultKeyStoreProviderException(
"Could not create temporary file", e);
} catch (NoSuchAlgorithmException e) {
throw new VaultKeyStoreProviderException("Could not load message digest");
}
// Prepare the keydata
KeyData keyData = new KeyData(retrieved.get(keyNamePrivateKey),
retrieved.get(keyNameCertificateChain), password);
KeyStoreData keyStoreData = new KeyStoreData(target.toPath(), password);
keyStoreData.putEntry(KEY_ALIAS, keyData);
creator.createKeystore(keyStoreData, true);
// Generate the result map
Map result = new HashMap<>();
result.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, target.getAbsolutePath());
result.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, password.value());
result.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, password.value());
result.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, truststoreLocation);
result.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, truststorePassword.value());
// remove the generated values from keys
keys.removeIf(result::containsKey);
if (keys.isEmpty()) {
for (Entry entry : retrieved.entrySet()) {
result.putIfAbsent(entry.getKey(), entry.getValue());
}
} else {
for (String wanted : keys) {
if (retrieved.containsKey(wanted)) {
result.putIfAbsent(wanted, retrieved.get(wanted));
} else {
throw new VaultKeyStoreProviderException(
"Did not find required key: " + wanted);
}
}
}
// Remove the original private key and certificate chain
result.remove(keyNamePrivateKey);
result.remove(keyNameCertificateChain);
logMap("Returning data", result);
return new ConfigData(result);
}
private void logMap(String message, Map result) {
if (LOG.isDebugEnabled()) {
StringBuilder content = new StringBuilder()
.append(String.format("%s%n", message));
List logKeys = new ArrayList<>(result.keySet());
Collections.sort(logKeys);
for (String key : logKeys) {
content.append(String.format("\t'%s' = '%s'%n", key, result.get(key)));
}
LOG.debug(content.toString());
}
}
String bytesToHexString(byte[] data) {
StringBuilder hexString = new StringBuilder(2 * data.length);
for (int i = 0; i < data.length; i++) {
String hex = Integer.toHexString(0xff & data[i]);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
/**
* Constructs the VaultHelper, build as utility method to enable spying during tests
*
* @param config The configuration to use when constructing the VaultHelper
* @return a new VaultHelper instance.
*/
VaultHelper createVaultHelper(VaultHelperConfig config) {
return new VaultHelper(config, LOG);
}
/**
* Closing the provider will remove the created vaultHelper.
*/
@Override
public void close() {
LOG.debug("Closing provider");
vaultHelper = Optional.empty();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy