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:
Tomas Dvorak
2024-06-20 09:49:57 +02:00
committed by GitHub
parent 85d1054f6a
commit fe71d42658
12 changed files with 377 additions and 204 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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)