diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java index dfbfd98317..e485ac6c18 100644 --- a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeConfigurationPeriodical.java @@ -16,6 +16,7 @@ */ package org.graylog.datanode.bootstrap.preflight; +import com.google.common.collect.ImmutableList; import org.bouncycastle.operator.OperatorException; import org.graylog.datanode.configuration.DatanodeConfiguration; import org.graylog.security.certutil.CertConstants; @@ -36,6 +37,7 @@ import org.graylog2.cluster.preflight.DataNodeProvisioningConfig; import org.graylog2.cluster.preflight.DataNodeProvisioningService; import org.graylog2.plugin.periodical.Periodical; import org.graylog2.plugin.system.NodeId; +import org.graylog2.shared.SuppressForbidden; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,8 +45,13 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import java.io.IOException; +import java.net.InetAddress; import java.security.KeyStore; +import java.util.Collections; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Singleton public class DataNodeConfigurationPeriodical extends Periodical { @@ -89,48 +96,92 @@ public class DataNodeConfigurationPeriodical extends Periodical { public void doRun() { LOG.debug("checking if this DataNode is supposed to take configuration steps."); var cfg = dataNodeProvisioningService.getPreflightConfigFor(nodeId.getNodeId()); - if (cfg == null) { + if (cfg.isEmpty()) { // write default config if none exists for this node - dataNodeProvisioningService.save(DataNodeProvisioningConfig.builder().nodeId(nodeId.getNodeId()).state(DataNodeProvisioningConfig.State.UNCONFIGURED).build()); - } else if (DataNodeProvisioningConfig.State.CONFIGURED.equals(cfg.state())) { + writeInitialProvisioningConfig(); + return; + } + cfg.ifPresent(c -> { + final var state = c.state(); + if (state == null) { + return; + } + switch (state) { + case CONFIGURED -> writeCsr(c); + case SIGNED -> readSignedCertificate(c); + } + }); + } + + private void readSignedCertificate(DataNodeProvisioningConfig cfg) { + if (cfg.certificate() == null) { + LOG.error("Config entry in signed state, but no certificate data present in Mongo"); + } else { try { - var node = nodeService.byNodeId(nodeId); - var csr = csrGenerator.generateCSR(passwordSecret, node.getHostname(), cfg.altNames(), privateKeyEncryptedStorage); - csrStorage.writeCsr(csr, nodeId.getNodeId()); - LOG.info("created CSR for this node"); - } catch (CSRGenerationException | IOException | NodeNotFoundException | OperatorException ex) { - LOG.error("error generating a CSR: " + ex.getMessage(), ex); - dataNodeProvisioningService.save(cfg.toBuilder().state(DataNodeProvisioningConfig.State.ERROR).errorMsg(ex.getMessage()).build()); - } - } else if (DataNodeProvisioningConfig.State.SIGNED.equals(cfg.state())) { - if (cfg.certificate() == null) { - LOG.error("Config entry in signed state, but no certificate data present in Mongo"); - } else { - try { - final Optional certificateChain = certMongoStorage.readCertChain(nodeId.getNodeId()); - if (certificateChain.isPresent()) { - final char[] secret = passwordSecret; - KeyStore nodeKeystore = certificateAndPrivateKeyMerger.merge( - certificateChain.get(), - privateKeyEncryptedStorage, - secret, - secret, - CertConstants.DATANODE_KEY_ALIAS - ); + final Optional certificateChain = certMongoStorage.readCertChain(nodeId.getNodeId()); + if (certificateChain.isPresent()) { + final char[] secret = passwordSecret; + KeyStore nodeKeystore = certificateAndPrivateKeyMerger.merge( + certificateChain.get(), + privateKeyEncryptedStorage, + secret, + secret, + CertConstants.DATANODE_KEY_ALIAS + ); - final KeystoreMongoLocation location = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.DATA_NODE_KEYSTORE_COLLECTION); - keystoreStorage.writeKeyStore(location, nodeKeystore, secret, secret); + final KeystoreMongoLocation location = new KeystoreMongoLocation(nodeId.getNodeId(), KeystoreMongoCollections.DATA_NODE_KEYSTORE_COLLECTION); + keystoreStorage.writeKeyStore(location, nodeKeystore, secret, secret); - //should be in one transaction, but we miss transactions... - dataNodeProvisioningService.changeState(nodeId.getNodeId(), DataNodeProvisioningConfig.State.STORED); - } - } catch (Exception ex) { - LOG.error("Config entry in signed state, but wrong certificate data present in Mongo"); + //should be in one transaction, but we miss transactions... + dataNodeProvisioningService.changeState(nodeId.getNodeId(), DataNodeProvisioningConfig.State.STORED); } + } catch (Exception ex) { + LOG.error("Config entry in signed state, but wrong certificate data present in Mongo"); } } } + private void writeCsr(DataNodeProvisioningConfig cfg) { + try { + final var node = nodeService.byNodeId(nodeId); + final var altNames = ImmutableList.builder() + .addAll(Optional.ofNullable(cfg.altNames()).orElse(Collections.emptyList())) + .addAll(determineAltNames()) + .build(); + final var csr = csrGenerator.generateCSR(passwordSecret, node.getHostname(), altNames, privateKeyEncryptedStorage); + csrStorage.writeCsr(csr, nodeId.getNodeId()); + LOG.info("created CSR for this node"); + } catch (CSRGenerationException | IOException | NodeNotFoundException | OperatorException ex) { + LOG.error("error generating a CSR: " + ex.getMessage(), ex); + dataNodeProvisioningService.save(cfg.asError(ex.getMessage())); + } + } + + private void writeInitialProvisioningConfig() { + dataNodeProvisioningService.save(DataNodeProvisioningConfig.builder() + .nodeId(nodeId.getNodeId()) + .state(DataNodeProvisioningConfig.State.UNCONFIGURED) + .build()); + } + + private Iterable determineAltNames() { + return Stream.of("127.0.0.1", "::1") + .map(this::reverseLookup) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + @SuppressForbidden("Deliberate use of InetAddress#getHostName") + private String reverseLookup(String ipAddress) { + try { + final var inetAddress = InetAddress.getByName(ipAddress); + final var reverseLookup = inetAddress.getHostName(); + return reverseLookup.equals(ipAddress) ? null : reverseLookup; + } catch (Exception e) { + return null; + } + } + @Override protected Logger getLogger() { return LOG; diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalServiceImpl.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalServiceImpl.java index 07dc83aa99..c26155f4c3 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalServiceImpl.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalServiceImpl.java @@ -194,8 +194,10 @@ public class CertRenewalServiceImpl implements CertRenewalService { @Override public void initiateRenewalForNode(final String nodeId) { // write new state to MongoDB so that the DataNode picks it up and generates a new CSR request - var config = dataNodeProvisioningService.getPreflightConfigFor(nodeId); - dataNodeProvisioningService.save(config.toBuilder().state(DataNodeProvisioningConfig.State.CONFIGURED).build()); + var config = dataNodeProvisioningService.getPreflightConfigFor(nodeId) + .map(DataNodeProvisioningConfig::asConfigured) + .orElseThrow(() -> new IllegalStateException("No config found for data node " + nodeId)); + dataNodeProvisioningService.save(config); } @Override @@ -205,7 +207,7 @@ public class CertRenewalServiceImpl implements CertRenewalService { final var keystore = loadKeyStoreForNode(node); final var certificate = keystore.flatMap(this::getCertificateForNode); final var certValidUntil = certificate.map(cert -> cert.getNotAfter().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()); - final var config = getDataNodeProvisioningConfig(node); + final var config = getDataNodeProvisioningConfig(node).orElseThrow(() -> new IllegalStateException("No config found for data node " + node.getNodeId())); return new DataNode(node.getNodeId(), node.getType(), node.getTransportAddress(), @@ -217,7 +219,7 @@ public class CertRenewalServiceImpl implements CertRenewalService { }).toList(); } - private DataNodeProvisioningConfig getDataNodeProvisioningConfig(final Node node) { + private Optional getDataNodeProvisioningConfig(final Node node) { return dataNodeProvisioningService.getPreflightConfigFor(node.getNodeId()); } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java index 79b3a40f67..83c05dfafd 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrGenerator.java @@ -18,7 +18,6 @@ package org.graylog.security.certutil.csr; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; -import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.GeneralName; @@ -26,7 +25,6 @@ import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; import org.graylog.security.certutil.csr.exceptions.CSRGenerationException; import org.graylog.security.certutil.privatekey.PrivateKeyEncryptedStorage; @@ -54,19 +52,18 @@ public class CsrGenerator { final List altNames, final PrivateKeyEncryptedStorage privateKeyEncryptedStorage) throws CSRGenerationException { try { - KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); - java.security.KeyPair certKeyPair = keyGen.generateKeyPair(); + final var keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM); + final var certKeyPair = keyGen.generateKeyPair(); privateKeyEncryptedStorage.writeEncryptedKey(privateKeyPassword, certKeyPair.getPrivate()); - - PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder( + final var p10Builder = new JcaPKCS10CertificationRequestBuilder( new X500Principal("CN=" + principalName), certKeyPair.getPublic() ); - var names = new ArrayList<>(List.of(principalName)); - if(altNames != null) { + final var names = new ArrayList<>(List.of(principalName)); + if (altNames != null) { names.addAll(altNames); } diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrFileStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrFileStorage.java index 63c1c15b82..35a7320bb0 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrFileStorage.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrFileStorage.java @@ -18,7 +18,6 @@ package org.graylog.security.certutil.csr.storage; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; -import org.bouncycastle.operator.OperatorException; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import java.io.BufferedReader; @@ -36,18 +35,14 @@ public record CsrFileStorage(String csrFilename) implements CsrStorage { } @Override - public void writeCsr(PKCS10CertificationRequest csr) - throws IOException, OperatorException { - + public void writeCsr(PKCS10CertificationRequest csr) throws IOException { try (JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(new FileWriter(csrFilename, Charset.defaultCharset()))) { jcaPEMWriter.writeObject(csr); } } @Override - public PKCS10CertificationRequest readCsr() - throws IOException, OperatorException { - + public PKCS10CertificationRequest readCsr() throws IOException { Reader pemReader = new BufferedReader(new FileReader(csrFilename, Charset.defaultCharset())); PEMParser pemParser = new PEMParser(pemReader); Object parsedObj = pemParser.readObject(); diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrMongoStorage.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrMongoStorage.java index b357ccadb5..306857f916 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrMongoStorage.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/storage/CsrMongoStorage.java @@ -20,13 +20,11 @@ import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.bouncycastle.operator.OperatorException; import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.graylog2.cluster.preflight.DataNodeProvisioningConfig; import org.graylog2.cluster.preflight.DataNodeProvisioningService; import javax.inject.Inject; import java.io.BufferedReader; import java.io.IOException; -import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.util.Optional; @@ -50,19 +48,18 @@ public class CsrMongoStorage { } - public Optional readCsr(String nodeId) throws IOException, OperatorException { - final DataNodeProvisioningConfig preflightConfig = mongoService.getPreflightConfigFor(nodeId); - if (preflightConfig != null) { - final String csr = preflightConfig.csr(); - - if (csr != null) { - Reader pemReader = new BufferedReader(new StringReader(csr)); - PEMParser pemParser = new PEMParser(pemReader); - Object parsedObj = pemParser.readObject(); - if (parsedObj instanceof PKCS10CertificationRequest) { - return Optional.of((PKCS10CertificationRequest) parsedObj); - } - } + public Optional readCsr(String nodeId) throws IOException { + final var nodeCsr = mongoService.getPreflightConfigFor(nodeId) + .flatMap(cfg -> Optional.ofNullable(cfg.csr())); + if (nodeCsr.isEmpty()) { + return Optional.empty(); + } + final var csr = nodeCsr.get(); + final var pemReader = new BufferedReader(new StringReader(csr)); + final var pemParser = new PEMParser(pemReader); + final var parsedObj = pemParser.readObject(); + if (parsedObj instanceof PKCS10CertificationRequest) { + return Optional.of((PKCS10CertificationRequest) parsedObj); } return Optional.empty(); } diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java index d60bf69fa0..6bbda39865 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/PreflightResource.java @@ -155,7 +155,7 @@ public class PreflightResource { public void addParameters(@PathParam("nodeID") String nodeID, @NotNull CertParameters params) { var cfg = dataNodeProvisioningService.getPreflightConfigFor(nodeID); - var builder = cfg != null ? cfg.toBuilder() : DataNodeProvisioningConfig.builder().nodeId(nodeID); + var builder = cfg.map(DataNodeProvisioningConfig::toBuilder).orElse(DataNodeProvisioningConfig.builder().nodeId(nodeID)); builder.altNames(params.altNames()).validFor(params.validFor()); dataNodeProvisioningService.save(builder.build()); diff --git a/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningBusEvents.java b/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningBusEvents.java index c98f991930..c981104119 100644 --- a/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningBusEvents.java +++ b/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningBusEvents.java @@ -51,7 +51,7 @@ public class DataNodeProvisioningBusEvents implements DataNodeProvisioningServic } @Override - public DataNodeProvisioningConfig getPreflightConfigFor(String nodeId) { + public Optional getPreflightConfigFor(String nodeId) { return delegate.getPreflightConfigFor(nodeId); } diff --git a/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningService.java b/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningService.java index 9dd4feb49b..01b6ca49ca 100644 --- a/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningService.java +++ b/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningService.java @@ -21,7 +21,8 @@ import java.util.Optional; import java.util.stream.Stream; public interface DataNodeProvisioningService { - DataNodeProvisioningConfig getPreflightConfigFor(String nodeId); + Optional getPreflightConfigFor(String nodeId); + List findAllNodesThatNeedAttention(); void writeCsr(String nodeId, String csr); diff --git a/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningServiceImpl.java b/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningServiceImpl.java index f3c63ba3e2..d22e6e40b2 100644 --- a/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningServiceImpl.java +++ b/graylog2-server/src/main/java/org/graylog2/cluster/preflight/DataNodeProvisioningServiceImpl.java @@ -51,8 +51,8 @@ public class DataNodeProvisioningServiceImpl extends PaginatedDbService getPreflightConfigFor(String nodeId) { + return Optional.ofNullable(dbCollection.findOne(DBQuery.is(FIELD_NODEID, nodeId))); } @Override