From fe71d426584710ea0ba63fe486de9c98115c0697 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Thu, 20 Jun 2024 09:49:57 +0200 Subject: [PATCH] Datanode periodical for certificate renewals (#19618) * Datanode periodical for certificate renewals * 30 minutes period for datanode cert renewal periodical * logging adapted * code cleanup * Removed unused, simplified renewal on graylog server side * removed unused cert renewal endpoint --- .../datanode/bindings/GenericBindings.java | 3 + .../datanode/bindings/PeriodicalBindings.java | 2 + .../DataNodeCertRenewalPeriodical.java | 140 ++++++++++++++++++ .../configuration/DatanodeKeystore.java | 3 +- .../datanode/opensearch/CsrRequester.java | 75 +--------- .../datanode/opensearch/CsrRequesterImpl.java | 93 ++++++++++++ .../opensearch/OpensearchProcessService.java | 4 +- .../statemachine/OpensearchStateMachine.java | 3 +- .../DataNodeCertRenewalPeriodicalTest.java | 112 ++++++++++++++ .../security/certutil/CertRenewalService.java | 14 +- .../certutil/CertRenewalServiceImpl.java | 107 ++----------- .../rest/CertificateRenewalResource.java | 25 +--- 12 files changed, 377 insertions(+), 204 deletions(-) create mode 100644 data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeCertRenewalPeriodical.java create mode 100644 data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequesterImpl.java create mode 100644 data-node/src/test/java/org/graylog/datanode/bootstrap/preflight/DataNodeCertRenewalPeriodicalTest.java diff --git a/data-node/src/main/java/org/graylog/datanode/bindings/GenericBindings.java b/data-node/src/main/java/org/graylog/datanode/bindings/GenericBindings.java index f1ca590da1..ee21625ffb 100644 --- a/data-node/src/main/java/org/graylog/datanode/bindings/GenericBindings.java +++ b/data-node/src/main/java/org/graylog/datanode/bindings/GenericBindings.java @@ -17,6 +17,8 @@ package org.graylog.datanode.bindings; import com.google.common.util.concurrent.ServiceManager; +import org.graylog.datanode.opensearch.CsrRequester; +import org.graylog.datanode.opensearch.CsrRequesterImpl; import org.graylog.security.certutil.CaService; import org.graylog.security.certutil.CaServiceImpl; import org.graylog2.plugin.inject.Graylog2Module; @@ -37,6 +39,7 @@ public class GenericBindings extends Graylog2Module { bind(ServiceManager.class).toProvider(ServiceManagerProvider.class).asEagerSingleton(); bind(X509TrustManager.class).to(CustomCAX509TrustManager.class).asEagerSingleton(); bind(CaService.class).to(CaServiceImpl.class); + bind(CsrRequester.class).to(CsrRequesterImpl.class); } } diff --git a/data-node/src/main/java/org/graylog/datanode/bindings/PeriodicalBindings.java b/data-node/src/main/java/org/graylog/datanode/bindings/PeriodicalBindings.java index 84b8e00712..f0ff3bed4e 100644 --- a/data-node/src/main/java/org/graylog/datanode/bindings/PeriodicalBindings.java +++ b/data-node/src/main/java/org/graylog/datanode/bindings/PeriodicalBindings.java @@ -18,6 +18,7 @@ package org.graylog.datanode.bindings; import com.google.inject.AbstractModule; import com.google.inject.multibindings.Multibinder; +import org.graylog.datanode.bootstrap.preflight.DataNodeCertRenewalPeriodical; import org.graylog.datanode.bootstrap.preflight.DataNodeConfigurationPeriodical; import org.graylog.datanode.periodicals.MetricsCollector; import org.graylog.datanode.periodicals.NodePingPeriodical; @@ -36,6 +37,7 @@ public class PeriodicalBindings extends AbstractModule { // periodicalBinder.addBinding().to(UserSessionTerminationPeriodical.class); periodicalBinder.addBinding().to(NodePingPeriodical.class); periodicalBinder.addBinding().to(DataNodeConfigurationPeriodical.class); + periodicalBinder.addBinding().to(DataNodeCertRenewalPeriodical.class); periodicalBinder.addBinding().to(MetricsCollector.class); } } diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeCertRenewalPeriodical.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeCertRenewalPeriodical.java new file mode 100644 index 0000000000..42bedf34f5 --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/preflight/DataNodeCertRenewalPeriodical.java @@ -0,0 +1,140 @@ +/* + * 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 + * . + */ +package org.graylog.datanode.bootstrap.preflight; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.graylog.datanode.configuration.DatanodeKeystore; +import org.graylog.datanode.opensearch.CsrRequester; +import org.graylog2.plugin.certificates.RenewalPolicy; +import org.graylog2.plugin.cluster.ClusterConfigService; +import org.graylog2.plugin.periodical.Periodical; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Optional; +import java.util.function.Supplier; + +@Singleton +public class DataNodeCertRenewalPeriodical extends Periodical { + private static final Logger LOG = LoggerFactory.getLogger(DataNodeCertRenewalPeriodical.class); + private static final long CERT_RENEWAL_THRESHOLD_PERCENTAGE = 10; + public static final Duration PERIODICAL_DURATION = Duration.ofMinutes(30); + + private final DatanodeKeystore datanodeKeystore; + private final Supplier renewalPolicySupplier; + + private final CsrRequester csrRequester; + + @Inject + public DataNodeCertRenewalPeriodical(DatanodeKeystore datanodeKeystore, ClusterConfigService clusterConfigService, CsrRequester csrRequester) { + this(datanodeKeystore, () -> clusterConfigService.get(RenewalPolicy.class), csrRequester); + } + + protected DataNodeCertRenewalPeriodical(DatanodeKeystore datanodeKeystore, Supplier renewalPolicySupplier, CsrRequester csrRequester) { + this.datanodeKeystore = datanodeKeystore; + this.renewalPolicySupplier = renewalPolicySupplier; + this.csrRequester = csrRequester; + } + + + @Override + public void doRun() { + // always check if there are any certificates that we can accept + getRenewalPolicy() + .filter(this::needsNewCertificate) + .ifPresent(renewalPolicy -> { + switch (renewalPolicy.mode()) { + case AUTOMATIC -> automaticRenewal(); + case MANUAL -> manualRenewal(); + } + }); + + } + + private void manualRenewal() { + LOG.debug("Manual renewal, ignoring on the datanode side for now"); + } + + private void automaticRenewal() { + csrRequester.triggerCertificateSigningRequest(); + } + + private boolean needsNewCertificate(RenewalPolicy renewalPolicy) { + final Date expiration = datanodeKeystore.getCertificateExpiration(); + return expiration == null || expiresSoon(expiration, renewalPolicy); + } + + private boolean expiresSoon(Date expiration, RenewalPolicy renewalPolicy) { + final Duration threshold = Duration.parse(renewalPolicy.certificateLifetime()).dividedBy(CERT_RENEWAL_THRESHOLD_PERCENTAGE); + final Instant renewalMoment = expiration.toInstant() + .minus(threshold) + .minus(PERIODICAL_DURATION); + final boolean expiresSoon = Instant.now().isAfter(renewalMoment); + if (expiresSoon) { + LOG.info("Datanode certificate will be renewed now, expiring soon (" + expiration + ")"); + } + return expiresSoon; + } + + private Optional getRenewalPolicy() { + return Optional.ofNullable(renewalPolicySupplier.get()); + } + + @Override + protected Logger getLogger() { + return LOG; + } + + @Override + public boolean runsForever() { + return false; + } + + @Override + public boolean stopOnGracefulShutdown() { + return true; + } + + @Override + public boolean leaderOnly() { + return false; + } + + @Override + public boolean startOnThisNode() { + return true; + } + + @Override + public boolean isDaemon() { + return true; + } + + @Override + public int getInitialDelaySeconds() { + return 0; + } + + @Override + public int getPeriodSeconds() { + return (int) PERIODICAL_DURATION.toSeconds(); + } +} diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystore.java b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystore.java index c26a744a8b..2d9a7e886d 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystore.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeKeystore.java @@ -51,7 +51,6 @@ import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Date; import java.util.List; -import java.util.Optional; import java.util.function.Supplier; import static org.graylog.security.certutil.CertConstants.PKCS12; @@ -80,7 +79,7 @@ public class DatanodeKeystore { this(configuration.datanodeDirectories(), passwordSecret, eventBus); } - DatanodeKeystore(DatanodeDirectories datanodeDirectories, String passwordSecret, EventBus eventBus) { + public DatanodeKeystore(DatanodeDirectories datanodeDirectories, String passwordSecret, EventBus eventBus) { this.datanodeDirectories = datanodeDirectories; this.passwordSecret = passwordSecret; this.eventBus = eventBus; diff --git a/data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequester.java b/data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequester.java index 36354fe0df..c2b325a6bc 100644 --- a/data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequester.java +++ b/data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequester.java @@ -16,77 +16,6 @@ */ package org.graylog.datanode.opensearch; -import com.google.common.collect.ImmutableList; -import jakarta.inject.Inject; -import org.bouncycastle.pkcs.PKCS10CertificationRequest; -import org.graylog.datanode.configuration.DatanodeKeystore; -import org.graylog.datanode.configuration.DatanodeKeystoreException; -import org.graylog.security.certutil.csr.exceptions.CSRGenerationException; -import org.graylog2.cluster.NodeNotFoundException; -import org.graylog2.cluster.certificates.CertificateExchange; -import org.graylog2.cluster.certificates.CertificateSigningRequest; -import org.graylog2.cluster.nodes.DataNodeDto; -import org.graylog2.cluster.nodes.NodeService; -import org.graylog2.plugin.system.NodeId; -import org.graylog2.shared.SuppressForbidden; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.InetAddress; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class CsrRequester { - - private static final Logger LOG = LoggerFactory.getLogger(OpensearchProcessService.class); - - private final NodeService nodeService; - private final NodeId nodeId; - - private final DatanodeKeystore datanodeKeystore; - - private final CertificateExchange certificateExchange; - - @Inject - public CsrRequester(NodeService nodeService, NodeId nodeId, DatanodeKeystore datanodeKeystore, CertificateExchange certificateExchange) { - this.nodeService = nodeService; - this.nodeId = nodeId; - this.datanodeKeystore = datanodeKeystore; - this.certificateExchange = certificateExchange; - } - - public void triggerCsr() { - try { - final var node = nodeService.byNodeId(nodeId); - final var altNames = ImmutableList.builder() - .addAll(determineAltNames()) - .build(); - final PKCS10CertificationRequest csr = datanodeKeystore.createCertificateSigningRequest(node.getHostname(), altNames); - certificateExchange.requestCertificate(new CertificateSigningRequest(nodeId.getNodeId(), csr)); - LOG.info("created CSR for this node"); - } catch (CSRGenerationException | IOException | NodeNotFoundException | DatanodeKeystoreException ex) { - throw new RuntimeException(ex); - } - } - - - 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; - } - } +public interface CsrRequester { + void triggerCertificateSigningRequest(); } diff --git a/data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequesterImpl.java b/data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequesterImpl.java new file mode 100644 index 0000000000..1ff7d60e2a --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/opensearch/CsrRequesterImpl.java @@ -0,0 +1,93 @@ +/* + * 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 + * . + */ +package org.graylog.datanode.opensearch; + +import com.google.common.collect.ImmutableList; +import jakarta.inject.Inject; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.graylog.datanode.Configuration; +import org.graylog.datanode.configuration.DatanodeConfiguration; +import org.graylog.datanode.configuration.DatanodeKeystore; +import org.graylog.datanode.configuration.DatanodeKeystoreException; +import org.graylog.security.certutil.csr.exceptions.CSRGenerationException; +import org.graylog2.cluster.NodeNotFoundException; +import org.graylog2.cluster.certificates.CertificateExchange; +import org.graylog2.cluster.certificates.CertificateSigningRequest; +import org.graylog2.cluster.nodes.DataNodeDto; +import org.graylog2.cluster.nodes.NodeService; +import org.graylog2.plugin.system.NodeId; +import org.graylog2.shared.SuppressForbidden; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CsrRequesterImpl implements CsrRequester { + + private static final Logger LOG = LoggerFactory.getLogger(OpensearchProcessService.class); + + private final NodeId nodeId; + + private final DatanodeKeystore datanodeKeystore; + + private final CertificateExchange certificateExchange; + private final String hostname; + + @Inject + public CsrRequesterImpl(Configuration datanodeConfiguration, NodeId nodeId, DatanodeKeystore datanodeKeystore, CertificateExchange certificateExchange) { + this.hostname = datanodeConfiguration.getHostname(); + this.nodeId = nodeId; + this.datanodeKeystore = datanodeKeystore; + this.certificateExchange = certificateExchange; + } + + public void triggerCertificateSigningRequest() { + try { + final var altNames = ImmutableList.builder() + .addAll(determineAltNames()) + .build(); + final PKCS10CertificationRequest csr = datanodeKeystore.createCertificateSigningRequest(hostname, altNames); + certificateExchange.requestCertificate(new CertificateSigningRequest(nodeId.getNodeId(), csr)); + LOG.info("Triggered certificate signing request for this datanode"); + } catch (CSRGenerationException | IOException | DatanodeKeystoreException ex) { + throw new RuntimeException(ex); + } + } + + + 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; + } + } +} diff --git a/data-node/src/main/java/org/graylog/datanode/opensearch/OpensearchProcessService.java b/data-node/src/main/java/org/graylog/datanode/opensearch/OpensearchProcessService.java index 64d5790497..6d0a2c3548 100644 --- a/data-node/src/main/java/org/graylog/datanode/opensearch/OpensearchProcessService.java +++ b/data-node/src/main/java/org/graylog/datanode/opensearch/OpensearchProcessService.java @@ -95,11 +95,11 @@ public class OpensearchProcessService extends AbstractIdleService implements Pro case START -> stateMachine.fire(OpensearchEvent.PROCESS_STARTED); case REQUEST_CSR -> { this.processAutostart = false; - csrRequester.triggerCsr(); + csrRequester.triggerCertificateSigningRequest(); } case REQUEST_CSR_WITH_AUTOSTART -> { this.processAutostart = true; - csrRequester.triggerCsr(); + csrRequester.triggerCertificateSigningRequest(); } } } diff --git a/data-node/src/main/java/org/graylog/datanode/opensearch/statemachine/OpensearchStateMachine.java b/data-node/src/main/java/org/graylog/datanode/opensearch/statemachine/OpensearchStateMachine.java index 9d5066d858..ff7cfcd6da 100644 --- a/data-node/src/main/java/org/graylog/datanode/opensearch/statemachine/OpensearchStateMachine.java +++ b/data-node/src/main/java/org/graylog/datanode/opensearch/statemachine/OpensearchStateMachine.java @@ -108,7 +108,8 @@ public class OpensearchStateMachine extends StateMachine. + */ +package org.graylog.datanode.bootstrap.preflight; + +import com.google.common.eventbus.EventBus; +import jakarta.annotation.Nonnull; +import org.assertj.core.api.Assertions; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.graylog.datanode.configuration.DatanodeDirectories; +import org.graylog.datanode.configuration.DatanodeKeystore; +import org.graylog.datanode.opensearch.CsrRequester; +import org.graylog.security.certutil.CertRequest; +import org.graylog.security.certutil.CertificateGenerator; +import org.graylog.security.certutil.KeyPair; +import org.graylog.security.certutil.cert.CertificateChain; +import org.graylog.security.certutil.csr.CsrSigner; +import org.graylog2.plugin.certificates.RenewalPolicy; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.List; +import java.util.function.Supplier; + +class DataNodeCertRenewalPeriodicalTest { + + + private Path tempDir; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + this.tempDir = tempDir; + } + + @Test + void testAlreadyExpired() throws Exception { + final DatanodeKeystore datanodeKeystore = datanodeKeystore(Duration.ofNanos(1)); + final CsrRequester csrRequester = Mockito.mock(CsrRequester.class); + final DataNodeCertRenewalPeriodical periodical = new DataNodeCertRenewalPeriodical(datanodeKeystore, autoRenewalPolic(Duration.ofMinutes(1)), csrRequester); + periodical.doRun(); + Mockito.verify(csrRequester, Mockito.times(1)).triggerCertificateSigningRequest(); + } + + + @Test + void testExpiringSoon() throws Exception { + final DatanodeKeystore datanodeKeystore = datanodeKeystore(Duration.ofMinutes(1)); + final CsrRequester csrRequester = Mockito.mock(CsrRequester.class); + final DataNodeCertRenewalPeriodical periodical = new DataNodeCertRenewalPeriodical(datanodeKeystore, autoRenewalPolic(Duration.ofMinutes(1)), csrRequester); + periodical.doRun(); + Mockito.verify(csrRequester, Mockito.times(1)).triggerCertificateSigningRequest(); + } + + + @Test + void testExpiringInFarFuture() throws Exception { + final DatanodeKeystore datanodeKeystore = datanodeKeystore(Duration.ofDays(30)); + final CsrRequester csrRequester = Mockito.mock(CsrRequester.class); + final DataNodeCertRenewalPeriodical periodical = new DataNodeCertRenewalPeriodical(datanodeKeystore, autoRenewalPolic(Duration.ofMinutes(30)), csrRequester); + periodical.doRun(); + Mockito.verify(csrRequester, Mockito.never()).triggerCertificateSigningRequest(); + } + + @Nonnull + private static Supplier autoRenewalPolic(Duration duration) { + return () -> new RenewalPolicy(RenewalPolicy.Mode.AUTOMATIC, duration.toString()); + } + + + private DatanodeKeystore datanodeKeystore(Duration certValidity) throws Exception { + final DatanodeKeystore datanodeKeystore = new DatanodeKeystore(new DatanodeDirectories(tempDir, tempDir, tempDir, tempDir), "foobar", new EventBus()); + datanodeKeystore.create(generateKeyPair(certValidity)); + + final PKCS10CertificationRequest csr = datanodeKeystore.createCertificateSigningRequest("my-hostname", List.of("second-hostname")); + Assertions.assertThat(csr.getSubject().toString()).isEqualTo("CN=my-hostname"); + + final CsrSigner signer = new CsrSigner(); + final KeyPair ca = CertificateGenerator.generate(CertRequest.selfSigned("Graylog CA").isCA(true).validity(Duration.ofDays(365))); + final X509Certificate datanodeCert = signer.sign(ca.privateKey(), ca.certificate(), csr, (int) certValidity.toDays()); + final CertificateChain certChain = new CertificateChain(datanodeCert, List.of(ca.certificate())); + + datanodeKeystore.replaceCertificatesInKeystore(certChain); + + return datanodeKeystore; + } + + + private KeyPair generateKeyPair(Duration validity) throws Exception { + final CertRequest certRequest = CertRequest.selfSigned(DatanodeKeystore.DATANODE_KEY_ALIAS) + .isCA(false) + .validity(validity); + return CertificateGenerator.generate(certRequest); + } +} diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalService.java b/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalService.java index 9727b495ab..0b0d282400 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalService.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/CertRenewalService.java @@ -16,28 +16,16 @@ */ package org.graylog.security.certutil; -import org.graylog2.cluster.nodes.DataNodeStatus; import org.graylog2.cluster.preflight.DataNodeProvisioningConfig; import java.time.LocalDateTime; -import java.util.List; +@Deprecated public interface CertRenewalService { record ProvisioningInformation(DataNodeProvisioningConfig.State status, String errorMsg, LocalDateTime certValidUntil) { } - record DataNode(String nodeId, DataNodeStatus dataNodeStatus, String transportAddress, - DataNodeProvisioningConfig.State status, String errorMsg, String hostname, String shortNodeId, - LocalDateTime certValidUntil) { - public String getId() { - return nodeId; - } - } - void checkCertificatesForRenewal(); - void initiateRenewalForNode(String nodeId); - List findNodes(); - } 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 a90bb341b8..133c62aa3b 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 @@ -18,21 +18,14 @@ package org.graylog.security.certutil; import com.google.common.annotations.VisibleForTesting; import jakarta.inject.Inject; -import jakarta.inject.Named; import jakarta.inject.Singleton; import org.graylog.scheduler.DBJobTriggerService; import org.graylog.scheduler.JobTriggerDto; import org.graylog.scheduler.clock.JobSchedulerClock; import org.graylog.security.certutil.ca.exceptions.KeyStoreStorageException; -import org.graylog.security.certutil.keystore.storage.KeystoreMongoStorage; -import org.graylog.security.certutil.keystore.storage.location.KeystoreMongoLocation; import org.graylog2.cluster.Node; -import org.graylog2.cluster.NodeNotFoundException; import org.graylog2.cluster.nodes.DataNodeDto; import org.graylog2.cluster.nodes.NodeService; -import org.graylog2.cluster.preflight.DataNodeProvisioningConfig; -import org.graylog2.datanode.DataNodeCommandService; -import org.graylog2.datanode.DatanodeStartType; import org.graylog2.notifications.Notification; import org.graylog2.notifications.NotificationService; import org.graylog2.plugin.certificates.RenewalPolicy; @@ -42,7 +35,6 @@ import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateExpiredException; @@ -54,7 +46,6 @@ import java.time.ZoneId; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import static org.graylog.security.certutil.CertConstants.CA_KEY_ALIAS; @@ -69,46 +60,37 @@ public class CertRenewalServiceImpl implements CertRenewalService { private static final Logger LOG = LoggerFactory.getLogger(CertRenewalServiceImpl.class); private final ClusterConfigService clusterConfigService; - private final KeystoreMongoStorage keystoreMongoStorage; private final NodeService nodeService; - private final DataNodeCommandService dataNodeCommandService; private final NotificationService notificationService; private final DBJobTriggerService jobTriggerService; private final JobSchedulerClock clock; private final CaService caService; - private final char[] passwordSecret; // TODO: convert to config? private long CERT_RENEWAL_THRESHOLD_PERCENTAGE = 10; @Inject public CertRenewalServiceImpl(final ClusterConfigService clusterConfigService, - final KeystoreMongoStorage keystoreMongoStorage, final NodeService nodeService, - final DataNodeCommandService dataNodeCommandService, final NotificationService notificationService, final DBJobTriggerService jobTriggerService, final CaService caService, - final JobSchedulerClock clock, - final @Named("password_secret") String passwordSecret) { + final JobSchedulerClock clock) { this.clusterConfigService = clusterConfigService; - this.keystoreMongoStorage = keystoreMongoStorage; this.nodeService = nodeService; - this.dataNodeCommandService = dataNodeCommandService; this.notificationService = notificationService; this.jobTriggerService = jobTriggerService; this.clock = clock; this.caService = caService; - this.passwordSecret = passwordSecret.toCharArray(); } @VisibleForTesting CertRenewalServiceImpl(final JobSchedulerClock clock) { - this(null, null, null, null, null, null, null, clock, "dummy"); + this(null, null, null, null, null, clock); } - private RenewalPolicy getRenewalPolicy() { - return this.clusterConfigService.get(RenewalPolicy.class); + private Optional getRenewalPolicy() { + return Optional.ofNullable(this.clusterConfigService.get(RenewalPolicy.class)); } boolean needsRenewal(final DateTime nextRenewal, final RenewalPolicy renewalPolicy, final X509Certificate cert) { @@ -139,13 +121,10 @@ public class CertRenewalServiceImpl implements CertRenewalService { @Override public void checkCertificatesForRenewal() { - final var renewalPolicy = getRenewalPolicy(); - - // a renewal policy has to be present to check for outdated certificates - if (renewalPolicy != null) { + getRenewalPolicy().ifPresent(renewalPolicy -> { checkDataNodesCertificatesForRenewal(renewalPolicy); checkCaCertificatesForRenewal(renewalPolicy); - } + }); } private DateTime getNextRenewal() { @@ -168,22 +147,6 @@ public class CertRenewalServiceImpl implements CertRenewalService { } } - private Optional loadKeyStoreForNode(Node node) { - try { - return keystoreMongoStorage.readKeyStore(KeystoreMongoLocation.datanode(node.getNodeId()), passwordSecret); - } catch (KeyStoreStorageException e) { - return Optional.empty(); - } - } - - private Optional getCertificateForNode(KeyStore keyStore) { - try { - return Optional.of((X509Certificate) keyStore.getCertificate(CertConstants.DATANODE_KEY_ALIAS)); - } catch (KeyStoreException e) { - return Optional.empty(); - } - } - protected List findNodesThatNeedCertificateRenewal(final RenewalPolicy renewalPolicy) { final var nextRenewal = getNextRenewal(); final Map activeDataNodes = nodeService.allActive(); @@ -195,50 +158,6 @@ public class CertRenewalServiceImpl implements CertRenewalService { }).toList(); } - @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 - try { - dataNodeCommandService.triggerCertificateSigningRequest(nodeId, DatanodeStartType.AUTOMATICALLY); - } catch (NodeNotFoundException e) { - throw new RuntimeException(e); - } - } - - @Override - public List findNodes() { - final Map activeDataNodes = nodeService.allActive(); - return activeDataNodes.values().stream().map(node -> { - 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()); - return new DataNode(node.getNodeId(), - node.getDataNodeStatus(), - node.getTransportAddress(), - state(node), - errorMessage(node), - node.getHostname(), - node.getShortNodeId(), - certValidUntil.orElse(null)); - }).toList(); - } - - @Deprecated - private String errorMessage(DataNodeDto node) { - return null; // TODO!!! - } - - @Deprecated - private DataNodeProvisioningConfig.State state(DataNodeDto node) { - return switch (node.getDataNodeStatus()) { - case AVAILABLE -> DataNodeProvisioningConfig.State.CONNECTED; - case STARTING -> DataNodeProvisioningConfig.State.CONNECTING; - case PREPARED -> DataNodeProvisioningConfig.State.CONNECTING; - case UNAVAILABLE -> DataNodeProvisioningConfig.State.ERROR; - default -> DataNodeProvisioningConfig.State.UNCONFIGURED; - }; - } - private void notifyManualRenewalForNode(final List nodes) { final var key = String.join(",", nodes.stream().map(Node::getNodeId).toList()); if (!notificationService.isFirst(Notification.Type.CERTIFICATE_NEEDS_RENEWAL)) { @@ -253,15 +172,11 @@ public class CertRenewalServiceImpl implements CertRenewalService { } protected void checkDataNodesCertificatesForRenewal(final RenewalPolicy renewalPolicy) { - final var nodes = findNodesThatNeedCertificateRenewal(renewalPolicy); - if (nodes.isEmpty()) { - return; - } - - if (RenewalPolicy.Mode.AUTOMATIC.equals(renewalPolicy.mode())) { - nodes.forEach(node -> initiateRenewalForNode(node.getNodeId())); - } else { - notifyManualRenewalForNode(nodes); + if (RenewalPolicy.Mode.MANUAL.equals(renewalPolicy.mode())) { + final List nodes = findNodesThatNeedCertificateRenewal(renewalPolicy); + if (!nodes.isEmpty()) { + notifyManualRenewalForNode(nodes); + } } } } diff --git a/graylog2-server/src/main/java/org/graylog/security/rest/CertificateRenewalResource.java b/graylog2-server/src/main/java/org/graylog/security/rest/CertificateRenewalResource.java index f7828f72f3..c75691f795 100644 --- a/graylog2-server/src/main/java/org/graylog/security/rest/CertificateRenewalResource.java +++ b/graylog2-server/src/main/java/org/graylog/security/rest/CertificateRenewalResource.java @@ -19,7 +19,6 @@ package org.graylog.security.rest; import io.swagger.annotations.Api; import io.swagger.annotations.ApiParam; import jakarta.inject.Inject; -import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; @@ -27,32 +26,24 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.graylog.security.certutil.CertRenewalService; import org.graylog2.audit.AuditEventTypes; import org.graylog2.audit.jersey.AuditEvent; +import org.graylog2.cluster.NodeNotFoundException; +import org.graylog2.datanode.DataNodeCommandService; +import org.graylog2.datanode.DatanodeStartType; import org.graylog2.plugin.rest.PluginRestResource; import org.graylog2.shared.security.RestPermissions; -import java.util.List; - @Api(value = "Certificates") @Path("/certrenewal") @Produces(MediaType.APPLICATION_JSON) @RequiresAuthentication public class CertificateRenewalResource implements PluginRestResource { - private final CertRenewalService certRenewalService; + private final DataNodeCommandService dataNodeCommandService; @Inject - public CertificateRenewalResource(final CertRenewalService certRenewalService) { - this.certRenewalService = certRenewalService; - } - - @GET - // reusing permissions to be the same as for editing the renewal policy, which is below cluster configuration - @RequiresPermissions(RestPermissions.CLUSTER_CONFIG_ENTRY_READ) - public List listDataNodes() { - // Nodes are not filtered right now so that you can manually initiate a renewal for every node available - return certRenewalService.findNodes(); + public CertificateRenewalResource(DataNodeCommandService dataNodeCommandService) { + this.dataNodeCommandService = dataNodeCommandService; } @POST @@ -60,7 +51,7 @@ public class CertificateRenewalResource implements PluginRestResource { @AuditEvent(type = AuditEventTypes.CERTIFICATE_RENEWAL_MANUALLY_INITIATED) // reusing permissions to be the same as for editing the renewal policy, which is below cluster configuration @RequiresPermissions({RestPermissions.CLUSTER_CONFIG_ENTRY_CREATE, RestPermissions.CLUSTER_CONFIG_ENTRY_EDIT}) - public void initiateCertRenewalForNode(@ApiParam(name = "nodeID") @PathParam("nodeID") String nodeID) { - certRenewalService.initiateRenewalForNode(nodeID); + public void initiateCertRenewalForNode(@ApiParam(name = "nodeID") @PathParam("nodeID") String nodeID) throws NodeNotFoundException { + dataNodeCommandService.triggerCertificateSigningRequest(nodeID, DatanodeStartType.AUTOMATICALLY); } }