All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.axual.utilities.config.providers.VaultKeyStoreProvider Maven / Gradle / Ivy

Go to download

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