mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
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<RenewalPolicy> 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<RenewalPolicy> 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<RenewalPolicy> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DataNodeDto> nodeService;
|
||||
private final NodeId nodeId;
|
||||
|
||||
private final DatanodeKeystore datanodeKeystore;
|
||||
|
||||
private final CertificateExchange certificateExchange;
|
||||
|
||||
@Inject
|
||||
public CsrRequester(NodeService<DataNodeDto> 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.<String>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<String> 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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
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.<String>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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ public class OpensearchStateMachine extends StateMachine<OpensearchState, Opense
|
||||
.permit(OpensearchEvent.HEALTH_CHECK_OK, OpensearchState.AVAILABLE)
|
||||
.permit(OpensearchEvent.PROCESS_STOPPED, OpensearchState.TERMINATED)
|
||||
.permit(OpensearchEvent.PROCESS_PREPARED, OpensearchState.PREPARED) //restart if reconfigured
|
||||
.permit(OpensearchEvent.PROCESS_TERMINATED, OpensearchState.TERMINATED);
|
||||
.permit(OpensearchEvent.PROCESS_TERMINATED, OpensearchState.TERMINATED)
|
||||
.permit(OpensearchEvent.PROCESS_STARTED, OpensearchState.STARTING);
|
||||
|
||||
// final state, the process is not alive anymore, terminated on the operating system level
|
||||
config.configure(OpensearchState.TERMINATED)
|
||||
|
||||
Reference in New Issue
Block a user