Refactoring/keystores code simplification (#18951)

* Datanode preflight provisioning IT

* npe fix

* fixed naming

* Add test for uploaded CA in preflight provisioning

* Keystore code simplifications in datanode

* more deprecations

* KeystoreMongoLocation code cleanup

* Remove KeyPairChecker, simplify code

* code cleanup

---------

Co-authored-by: Matthias Oesterheld <33032967+moesterheld@users.noreply.github.com>
This commit is contained in:
Tomas Dvorak
2024-04-15 09:01:29 +02:00
committed by GitHub
parent b83fae8a5d
commit 7b71548345
29 changed files with 287 additions and 523 deletions

View File

@@ -20,12 +20,12 @@ import com.google.common.eventbus.EventBus;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.multibindings.OptionalBinder;
import jakarta.ws.rs.container.DynamicFeature;
import jakarta.ws.rs.ext.ExceptionMapper;
import org.graylog.datanode.Configuration;
import org.graylog.datanode.process.ProcessStateMachine;
import org.graylog.datanode.process.ProcessStateMachineProvider;
import org.graylog.datanode.shared.system.activities.DataNodeActivityWriter;
import org.graylog.security.certutil.keystore.storage.KeystoreContentMover;
import org.graylog.security.certutil.keystore.storage.SinglePasswordKeystoreContentMover;
import org.graylog2.bindings.providers.ClusterEventBusProvider;
import org.graylog2.cluster.ClusterConfigServiceImpl;
import org.graylog2.cluster.nodes.DataNodeClusterService;
@@ -40,9 +40,6 @@ import org.graylog2.plugin.inject.Graylog2Module;
import org.graylog2.shared.bindings.providers.EventBusProvider;
import org.graylog2.shared.system.activities.ActivityWriter;
import jakarta.ws.rs.container.DynamicFeature;
import jakarta.ws.rs.ext.ExceptionMapper;
public class ServerBindings extends Graylog2Module {
private final Configuration configuration;
private final boolean isMigrationCommand;
@@ -80,7 +77,6 @@ public class ServerBindings extends Graylog2Module {
private void bindSingletons() {
bind(ClusterConfigService.class).to(ClusterConfigServiceImpl.class).asEagerSingleton();
bind(KeystoreContentMover.class).to(SinglePasswordKeystoreContentMover.class).asEagerSingleton();
}
private void bindInterfaces() {

View File

@@ -17,6 +17,9 @@
package org.graylog.datanode.bootstrap.preflight;
import com.google.common.collect.ImmutableList;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.bouncycastle.operator.OperatorException;
import org.graylog.datanode.configuration.DatanodeConfiguration;
import org.graylog.security.certutil.CertConstants;
@@ -27,7 +30,7 @@ import org.graylog.security.certutil.csr.CertificateAndPrivateKeyMerger;
import org.graylog.security.certutil.csr.CsrGenerator;
import org.graylog.security.certutil.csr.exceptions.CSRGenerationException;
import org.graylog.security.certutil.csr.storage.CsrMongoStorage;
import org.graylog.security.certutil.keystore.storage.SmartKeystoreStorage;
import org.graylog.security.certutil.keystore.storage.KeystoreMongoStorage;
import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoCollections;
import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation;
import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedFileStorage;
@@ -42,10 +45,6 @@ import org.graylog2.shared.SuppressForbidden;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Path;
@@ -68,9 +67,10 @@ public class DataNodeConfigurationPeriodical extends Periodical {
private final CsrGenerator csrGenerator;
private final CertChainStorage certMongoStorage;
private final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger;
private final SmartKeystoreStorage keystoreStorage;
private final char[] passwordSecret;
private final KeystoreMongoStorage mongoKeyStorage;
@Inject
public DataNodeConfigurationPeriodical(final DataNodeProvisioningService dataNodeProvisioningService,
final NodeService<DataNodeDto> nodeService,
@@ -79,9 +79,8 @@ public class DataNodeConfigurationPeriodical extends Periodical {
final CsrGenerator csrGenerator,
final CertChainMongoStorage certMongoStorage,
final CertificateAndPrivateKeyMerger certificateAndPrivateKeyMerger,
final SmartKeystoreStorage keystoreStorage,
final @Named("password_secret") String passwordSecret,
final DatanodeConfiguration datanodeConfiguration) throws IOException {
final DatanodeConfiguration datanodeConfiguration, KeystoreMongoStorage mongoKeyStorage) throws IOException {
this.dataNodeProvisioningService = dataNodeProvisioningService;
this.nodeService = nodeService;
this.nodeId = nodeId;
@@ -89,10 +88,10 @@ public class DataNodeConfigurationPeriodical extends Periodical {
this.csrGenerator = csrGenerator;
this.certMongoStorage = certMongoStorage;
this.certificateAndPrivateKeyMerger = certificateAndPrivateKeyMerger;
this.keystoreStorage = keystoreStorage;
// TODO: merge with real storage
this.privateKeyEncryptedStorage = new PrivateKeyEncryptedFileStorage(datanodeConfiguration.datanodeDirectories().createConfigurationFile(Path.of("privateKey.cert")));
this.passwordSecret = passwordSecret.toCharArray();
this.mongoKeyStorage = mongoKeyStorage;
}
@Override
@@ -133,8 +132,7 @@ public class DataNodeConfigurationPeriodical extends Periodical {
CertConstants.DATANODE_KEY_ALIAS
);
final KeystoreMongoLocation location = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.DATA_NODE_KEYSTORE_COLLECTION);
keystoreStorage.writeKeyStore(location, nodeKeystore, secret, secret);
mongoKeyStorage.writeKeyStore(KeystoreMongoLocation.datanode(nodeId), nodeKeystore, secret, secret);
//should be in one transaction, but we miss transactions...
dataNodeProvisioningService.changeState(nodeId.getNodeId(), DataNodeProvisioningConfig.State.STORED);

View File

@@ -1,72 +0,0 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.datanode.configuration.certificates;
import org.apache.commons.lang3.RandomStringUtils;
import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException;
import org.graylog.security.certutil.keystore.storage.SmartKeystoreStorage;
import org.graylog.security.certutil.keystore.storage.location.KeystoreLocation;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.security.KeyStore;
import java.util.Optional;
public class KeystoreReEncryption {
private final SmartKeystoreStorage keystoreStorage;
private final String passwordSecret;
@Inject
public KeystoreReEncryption(final SmartKeystoreStorage keystoreStorage,
final @Named("password_secret") String passwordSecret) {
this.keystoreStorage = keystoreStorage;
this.passwordSecret = passwordSecret;
}
public void reEncyptWithSecret(final KeystoreLocation originalLocation,
final char[] originalPassword,
final KeystoreLocation targetLocation
) throws KeyStoreStorageException {
reEncypt(originalLocation, originalPassword, targetLocation, passwordSecret.toCharArray());
}
public char[] reEncyptWithOtp(final KeystoreLocation originalLocation,
final char[] originalPassword,
final KeystoreLocation targetLocation
) throws KeyStoreStorageException {
final char[] oneTimePassword = RandomStringUtils.randomAlphabetic(256).toCharArray();
reEncypt(originalLocation, originalPassword, targetLocation, oneTimePassword);
return oneTimePassword;
}
private void reEncypt(final KeystoreLocation originalLocation,
final char[] originalPassword,
final KeystoreLocation targetLocation,
final char[] newPassword
) throws KeyStoreStorageException {
final Optional<KeyStore> keyStore = keystoreStorage.readKeyStore(originalLocation, originalPassword);
if (keyStore.isPresent()) {
final KeyStore originalKeystore = keyStore.get();
keystoreStorage.writeKeyStore(targetLocation, originalKeystore, originalPassword, newPassword);
} else {
throw new KeyStoreStorageException("No keystore present in : " + originalLocation);
}
}
}

View File

@@ -16,46 +16,49 @@
*/
package org.graylog.datanode.configuration.variants;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull;
import org.graylog.datanode.Configuration;
import org.graylog.datanode.configuration.DatanodeConfiguration;
import org.graylog.datanode.configuration.certificates.KeystoreReEncryption;
import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException;
import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation;
import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoCollections;
import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage;
import org.graylog.security.certutil.keystore.storage.KeystoreMongoStorage;
import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation;
import org.graylog2.cluster.certificates.CertificatesService;
import org.graylog2.plugin.system.NodeId;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.IOException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Optional;
public final class MongoCertSecureConfiguration extends SecureConfiguration {
private final KeystoreReEncryption keystoreReEncryption;
private final NodeId nodeId;
private final CertificatesService certificatesService;
private final char[] secret;
private final KeystoreMongoLocation mongoLocation;
private final char[] mongoKeystorePassword;
private final KeystoreMongoStorage mongoKeyStorage;
private final KeystoreFileStorage fileKeyStorage;
@Inject
public MongoCertSecureConfiguration(final Configuration localConfiguration,
final DatanodeConfiguration datanodeConfiguration,
final KeystoreReEncryption keystoreReEncryption,
public MongoCertSecureConfiguration(final DatanodeConfiguration datanodeConfiguration,
final NodeId nodeId,
final @Named("password_secret") String passwordSecret,
final CertificatesService certificatesService
final CertificatesService certificatesService, KeystoreMongoStorage mongoKeyStore, KeystoreFileStorage fileKeyStore
) {
super(datanodeConfiguration);
this.keystoreReEncryption = keystoreReEncryption;
this.nodeId = nodeId;
this.certificatesService = certificatesService;
this.mongoLocation = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.DATA_NODE_KEYSTORE_COLLECTION);
this.secret = passwordSecret.toCharArray();
this.mongoKeyStorage = mongoKeyStore;
this.fileKeyStorage = fileKeyStore;
//TODO: matches line 123 of DataNodePreflightGeneratePeriodical, but both need to be changed
this.mongoKeystorePassword = secret;
@@ -63,22 +66,44 @@ public final class MongoCertSecureConfiguration extends SecureConfiguration {
@Override
public boolean isConfigured(Configuration localConfiguration) {
return certificatesService.hasCert(mongoLocation);
return certificatesService.hasCert(getMongoKeystoreLocation());
}
@Override
public OpensearchSecurityConfiguration build() throws KeyStoreStorageException, IOException, GeneralSecurityException {
final KeystoreFileLocation targetTransportKeystoreLocation = getTransportKeystoreLocation();
final KeystoreFileLocation targetHttpKeystoreLocation = getHttpKeystoreLocation();
final Path targetTransportKeystoreLocation = getTransportKeystoreLocation();
final Path targetHttpKeystoreLocation = getHttpKeystoreLocation();
// this will take the mongodb-stored keys and persist them on a disk, in the opensearch configuration directory
keystoreReEncryption.reEncyptWithSecret(mongoLocation, mongoKeystorePassword, targetTransportKeystoreLocation);
keystoreReEncryption.reEncyptWithSecret(mongoLocation, mongoKeystorePassword, targetHttpKeystoreLocation);
final KeystoreMongoLocation mongoKeystoreLocation = getMongoKeystoreLocation();
reEncypt(mongoKeystoreLocation, mongoKeystorePassword, targetTransportKeystoreLocation, secret);
reEncypt(mongoKeystoreLocation, mongoKeystorePassword, targetHttpKeystoreLocation, secret);
return new OpensearchSecurityConfiguration(
new KeystoreInformation(targetTransportKeystoreLocation.keystorePath().toAbsolutePath(), secret),
new KeystoreInformation(targetHttpKeystoreLocation.keystorePath().toAbsolutePath(), secret)
new KeystoreInformation(targetTransportKeystoreLocation.toAbsolutePath(), secret),
new KeystoreInformation(targetHttpKeystoreLocation.toAbsolutePath(), secret)
);
}
@NotNull
private KeystoreMongoLocation getMongoKeystoreLocation() {
return KeystoreMongoLocation.datanode(nodeId);
}
private void reEncypt(final KeystoreMongoLocation originalLocation,
final char[] originalPassword,
final Path targetLocation,
final char[] newPassword
) throws KeyStoreStorageException {
final Optional<KeyStore> keyStore = mongoKeyStorage.readKeyStore(originalLocation, originalPassword);
if (keyStore.isPresent()) {
final KeyStore originalKeystore = keyStore.get();
fileKeyStorage.writeKeyStore(targetLocation, originalKeystore, originalPassword, newPassword);
} else {
throw new KeyStoreStorageException("No keystore present in : " + originalLocation);
}
}
}

View File

@@ -18,7 +18,6 @@ package org.graylog.datanode.configuration.variants;
import com.google.common.base.Suppliers;
import org.graylog.datanode.configuration.DatanodeConfiguration;
import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation;
import java.io.IOException;
import java.nio.file.Path;
@@ -41,14 +40,13 @@ public sealed abstract class SecureConfiguration implements SecurityConfiguratio
*/
private static final Path TARGET_DATANODE_TRANSPORT_KEYSTORE_FILENAME = Path.of("transport-keystore.p12");
private final Supplier<KeystoreFileLocation> httpKeystoreLocation;
private final Supplier<KeystoreFileLocation> transportKeystoreLocation;
private final Supplier<Path> httpKeystoreLocation;
private final Supplier<Path> transportKeystoreLocation;
public SecureConfiguration(final DatanodeConfiguration datanodeConfiguration) {
this.httpKeystoreLocation = Suppliers.memoize(() -> {
try {
final Path filePath = datanodeConfiguration.datanodeDirectories().createOpensearchProcessConfigurationFile(TARGET_DATANODE_HTTP_KEYSTORE_FILENAME);
return new KeystoreFileLocation(filePath);
return datanodeConfiguration.datanodeDirectories().createOpensearchProcessConfigurationFile(TARGET_DATANODE_HTTP_KEYSTORE_FILENAME);
} catch (IOException e) {
throw new RuntimeException("Failed to create http keystore file", e);
}
@@ -56,20 +54,19 @@ public sealed abstract class SecureConfiguration implements SecurityConfiguratio
this.transportKeystoreLocation = Suppliers.memoize(() -> {
try {
final Path filePath = datanodeConfiguration.datanodeDirectories().createOpensearchProcessConfigurationFile(TARGET_DATANODE_TRANSPORT_KEYSTORE_FILENAME);
return new KeystoreFileLocation(filePath);
return datanodeConfiguration.datanodeDirectories().createOpensearchProcessConfigurationFile(TARGET_DATANODE_TRANSPORT_KEYSTORE_FILENAME);
} catch (IOException e) {
throw new RuntimeException("Failed to create transport keystore file", e);
}
});
}
KeystoreFileLocation getHttpKeystoreLocation() {
Path getHttpKeystoreLocation() {
return httpKeystoreLocation.get();
}
KeystoreFileLocation getTransportKeystoreLocation() {
Path getTransportKeystoreLocation() {
return transportKeystoreLocation.get();
}
}

View File

@@ -16,18 +16,19 @@
*/
package org.graylog.datanode.configuration.variants;
import jakarta.inject.Inject;
import org.apache.commons.lang3.RandomStringUtils;
import org.graylog.datanode.Configuration;
import org.graylog.datanode.configuration.DatanodeConfiguration;
import org.graylog.datanode.configuration.OpensearchConfigurationException;
import org.graylog.datanode.configuration.certificates.KeystoreReEncryption;
import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException;
import org.graylog.security.certutil.keystore.storage.location.KeystoreFileLocation;
import jakarta.inject.Inject;
import org.graylog.security.certutil.keystore.storage.KeystoreFileStorage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
@@ -41,16 +42,16 @@ public final class UploadedCertFilesSecureConfiguration extends SecureConfigurat
private final String uploadedHttpKeystoreFileName;
private final String datanodeTransportCertificatePassword;
private final String datanodeHttpCertificatePassword;
private final KeystoreReEncryption keystoreReEncryption;
private final KeystoreFileStorage keystoreFileStorage;
private final DatanodeConfiguration datanodeConfiguration;
@Inject
public UploadedCertFilesSecureConfiguration(final Configuration localConfiguration,
final DatanodeConfiguration datanodeConfiguration,
final KeystoreReEncryption keystoreReEncryption) {
KeystoreFileStorage keystoreFileStorage) {
super(datanodeConfiguration);
this.datanodeConfiguration = datanodeConfiguration;
this.keystoreReEncryption = keystoreReEncryption;
this.keystoreFileStorage = keystoreFileStorage;
this.uploadedTransportKeystoreFileName = localConfiguration.getDatanodeTransportCertificate();
this.uploadedHttpKeystoreFileName = localConfiguration.getDatanodeHttpCertificate();
@@ -116,20 +117,45 @@ public final class UploadedCertFilesSecureConfiguration extends SecureConfigurat
@Override
public OpensearchSecurityConfiguration build() throws KeyStoreStorageException, IOException, GeneralSecurityException {
final KeystoreFileLocation targetTransportKeystoreLocation = getTransportKeystoreLocation();
final KeystoreFileLocation targetHttpKeystoreLocation = getHttpKeystoreLocation();
final Path targetTransportKeystoreLocation = getTransportKeystoreLocation();
final Path targetHttpKeystoreLocation = getHttpKeystoreLocation();
final char[] transportOTP = keystoreReEncryption.reEncyptWithOtp(new KeystoreFileLocation(datanodeConfiguration.datanodeDirectories().resolveConfigurationSourceFile(uploadedTransportKeystoreFileName).orElseThrow(() -> new RuntimeException("This should not happen, certificate expected"))),
final char[] transportOTP = reEncyptWithOtp(datanodeConfiguration.datanodeDirectories().resolveConfigurationSourceFile(uploadedTransportKeystoreFileName).orElseThrow(() -> new RuntimeException("This should not happen, certificate expected")),
datanodeTransportCertificatePassword.toCharArray(),
targetTransportKeystoreLocation);
final char[] httpOTP = keystoreReEncryption.reEncyptWithOtp(new KeystoreFileLocation(datanodeConfiguration.datanodeDirectories().resolveConfigurationSourceFile(uploadedHttpKeystoreFileName).orElseThrow(() -> new RuntimeException("This should not happen, certificate expected"))),
final char[] httpOTP = reEncyptWithOtp(datanodeConfiguration.datanodeDirectories().resolveConfigurationSourceFile(uploadedHttpKeystoreFileName).orElseThrow(() -> new RuntimeException("This should not happen, certificate expected")),
datanodeHttpCertificatePassword.toCharArray(),
targetHttpKeystoreLocation);
return new OpensearchSecurityConfiguration(
new KeystoreInformation(targetTransportKeystoreLocation.keystorePath().toAbsolutePath(), transportOTP),
new KeystoreInformation(targetHttpKeystoreLocation.keystorePath().toAbsolutePath(), httpOTP)
new KeystoreInformation(targetTransportKeystoreLocation.toAbsolutePath(), transportOTP),
new KeystoreInformation(targetHttpKeystoreLocation.toAbsolutePath(), httpOTP)
);
}
public char[] reEncyptWithOtp(final Path originalLocation,
final char[] originalPassword,
final Path targetLocation
) throws KeyStoreStorageException {
// caution! This password changes during each configuration request!!!
final char[] oneTimePassword = RandomStringUtils.randomAlphabetic(256).toCharArray();
reEncypt(originalLocation, originalPassword, targetLocation, oneTimePassword);
return oneTimePassword;
}
private void reEncypt(final Path originalLocation,
final char[] originalPassword,
final Path targetLocation,
final char[] newPassword
) throws KeyStoreStorageException {
final Optional<KeyStore> keyStore = keystoreFileStorage.readKeyStore(originalLocation, originalPassword);
if (keyStore.isPresent()) {
final KeyStore originalKeystore = keyStore.get();
keystoreFileStorage.writeKeyStore(targetLocation, originalKeystore, originalPassword, newPassword);
} else {
throw new KeyStoreStorageException("No keystore present in : " + originalLocation);
}
}
}