Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

preflight check for password_secret #21491

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-21504.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "a"
message = "Add preflight check for data node and graylog server, veryfing that password_secret is configured to the same value everywhere."

pulls = ["21491"]
issues = ["21504"]
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.graylog.datanode.bindings;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.MapBinder;
import org.graylog.datanode.bootstrap.preflight.DatanodeDirectoriesLockfileCheck;
Expand All @@ -28,14 +29,23 @@
import org.graylog.datanode.opensearch.CsrRequesterImpl;
import org.graylog2.bindings.providers.MongoConnectionProvider;
import org.graylog2.bootstrap.preflight.MongoDBPreflightCheck;
import org.graylog2.bootstrap.preflight.PasswordSecretPreflightCheck;
import org.graylog2.bootstrap.preflight.PreflightCheck;
import org.graylog2.cluster.certificates.CertificateExchange;
import org.graylog2.cluster.certificates.CertificateExchangeImpl;
import org.graylog2.database.MongoConnection;
import org.graylog2.shared.plugins.ChainingClassLoader;
import org.graylog2.shared.plugins.GraylogClassLoader;

public class PreflightChecksBindings extends AbstractModule {


private final ChainingClassLoader chainingClassLoader;

public PreflightChecksBindings(ChainingClassLoader chainingClassLoader) {
this.chainingClassLoader = chainingClassLoader;
}

@Override
protected void configure() {
bind(CsrRequester.class).to(CsrRequesterImpl.class).asEagerSingleton();
Expand All @@ -47,14 +57,13 @@ protected void configure() {
addPreflightCheck(DatanodeDirectoriesLockfileCheck.class);
addPreflightCheck(OpenSearchPreconditionsCheck.class);
addPreflightCheck(OpensearchDataDirCompatibilityCheck.class);

addPreflightCheck(PasswordSecretPreflightCheck.class);
// Mongodb is needed for legacy datanode storage, where we want to extract the certificate chain from
// mongodb and store it in local keystore
bind(MongoConnection.class).toProvider(MongoConnectionProvider.class);
addPreflightCheck(DatanodeKeystoreCheck.class);
}


protected void addPreflightCheck(Class<? extends PreflightCheck> preflightCheck) {
preflightChecksBinder().addBinding(preflightCheck.getCanonicalName()).to(preflightCheck);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.bindings;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.graylog.grn.GRNRegistry;
import org.graylog2.jackson.InputConfigurationBeanDeserializerModifier;
import org.graylog2.security.encryption.EncryptedValueService;
import org.graylog2.shared.bindings.providers.ObjectMapperProvider;
import org.graylog2.shared.plugins.GraylogClassLoader;

import java.util.Collections;

/**
* This ObjectMapperProvider should be used only for preflight checks and preflight web. It's significantly limited.
* For all other usages, please refer to {@link ObjectMapperProvider}.
*/
public class PreflightObjectMapperProvider implements Provider<ObjectMapper> {

private final ObjectMapperProvider delegate;

@Inject
public PreflightObjectMapperProvider(@GraylogClassLoader ClassLoader classLoader, EncryptedValueService encryptedValueService) {
delegate = new ObjectMapperProvider(
classLoader,
Collections.emptySet(),
encryptedValueService,
GRNRegistry.createWithBuiltinTypes(),
InputConfigurationBeanDeserializerModifier.withoutConfig()
);
}

@Override
public ObjectMapper get() {
return delegate.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private Injector getPreflightInjector(List<Module> preflightCheckModules) {
new PreflightClusterConfigurationModule(chainingClassLoader),
new NamedConfigParametersOverrideModule(jadConfig.getConfigurationBeans()),
new ConfigurationModule(configuration),
new PreflightChecksBindings(),
new PreflightChecksBindings(chainingClassLoader),
new DatanodeConfigurationBindings(),
new Module() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
*/
package org.graylog.datanode.bootstrap.preflight;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.AbstractModule;
import org.graylog.datanode.bindings.PreflightObjectMapperProvider;
import org.graylog2.cluster.ClusterConfigServiceImpl;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.shared.plugins.ChainingClassLoader;
import org.graylog2.shared.plugins.GraylogClassLoader;

public class PreflightClusterConfigurationModule extends AbstractModule {
private final ChainingClassLoader chainingClassLoader;
Expand All @@ -32,5 +35,12 @@ public PreflightClusterConfigurationModule(ChainingClassLoader chainingClassLoad
protected void configure() {
bind(ChainingClassLoader.class).toInstance(chainingClassLoader);
bind(ClusterConfigService.class).to(ClusterConfigServiceImpl.class).asEagerSingleton();

bindLimitedObjectMapper();
}

private void bindLimitedObjectMapper() {
bind(ClassLoader.class).annotatedWith(GraylogClassLoader.class).toInstance(chainingClassLoader);
bind(ObjectMapper.class).toProvider(PreflightObjectMapperProvider.class).asEagerSingleton();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.graylog2.bootstrap.preflight;

import jakarta.inject.Inject;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.security.encryption.EncryptedValue;
import org.graylog2.security.encryption.EncryptedValueService;

import javax.annotation.Nonnull;
import java.util.Optional;

/**
* This preflight check exists to validate that every node has the same password_secret value configured.
* The first node in the cluster will persist a known and encoded secret in the cluster_config collection.
* Every other node is then testing if it's possible to read and decode the value. If not, then the password_secret
* is not matching and we'd get problems later during runtime. So it's better to stop the startup and report
* an explicit and readable error.
*/
public class PasswordSecretPreflightCheck implements PreflightCheck {


private static final String KNOWN_VALUE = "graylog";

private final ClusterConfigService clusterConfigService;

private final EncryptedValueService encryptionService;


@Inject
public PasswordSecretPreflightCheck(ClusterConfigService clusterConfigService, EncryptedValueService encryptionService) {
this.clusterConfigService = clusterConfigService;
this.encryptionService = encryptionService;
}

@Override
public void runCheck() throws PreflightCheckException {
final PreflightEncryptedSecret encryptedValue = clusterConfigService.get(PreflightEncryptedSecret.class);
Optional.ofNullable(encryptedValue)
.ifPresentOrElse(this::validateSecret, this::persistSecret);
}

private void persistSecret() {
final EncryptedValue encryptedValue = encryptionService.encrypt(KNOWN_VALUE);
clusterConfigService.write(new PreflightEncryptedSecret(encryptedValue));
}

private void validateSecret(@Nonnull PreflightEncryptedSecret preflightEncryptedSecret) {
final EncryptedValue encryptedSecret = preflightEncryptedSecret.encryptedSecret();

try {
final String decrypted = encryptionService.decrypt(encryptedSecret);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately, our implementation in AESTools doesn't guarantee that the secret is identical as it modifies the key. You can reproduce that by removing a single letter from the end of a long password secret. This will still lead to problems with the keystore password.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a problem related to this PR. For the mongodb entries encryption, this will detect any problem. Keystore reading will be a local problem that we can (and should) capture by proper error handling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure. I would expect the check to make sure that password_secrets are identical on all nodes, no matter if they are used for encryption or anything else. This is also what the class' javadoc implies.
With the given check, there is still a margin for error which is difficult to track down. Something that we wanted to avoid with this check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could we fix that?

Copy link
Contributor

@moesterheld moesterheld Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't thought it through completely yet, but what if we don't encrypt a constant value but the secret itself? Then, even if you can decrypt it with a shorter secret, you could still make sure that they are identical

if (!KNOWN_VALUE.equals(decrypted)) {
throwException();
}
} catch (Exception e) {
throwException();
}

}

private void throwException() {
throw new PreflightCheckException("""
Invalid password_secret!
Failed to decrypt values from MongoDB. This means that your password_secret has been changed or there
are some nodes in your cluster that are using a different password_secret to the one configured on this node. Secrets have to be configured
to the same value on every node and can't be changed afterwards.""");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.graylog2.bootstrap.preflight;

import org.graylog2.security.encryption.EncryptedValue;

public record PreflightEncryptedSecret(EncryptedValue encryptedSecret) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ protected void configure() {
// The MongoDBPreflightCheck is not registered here, because it is called separately from ServerBootstrap
addPreflightCheck(SearchDbPreflightCheck.class);
addPreflightCheck(DiskJournalPreflightCheck.class);
addPreflightCheck(PasswordSecretPreflightCheck.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.graylog2.bootstrap.preflight;


import jakarta.annotation.Nonnull;
import org.apache.commons.lang3.RandomStringUtils;
import org.assertj.core.api.Assertions;
import org.graylog.security.certutil.InMemoryClusterConfigService;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.security.encryption.EncryptedValueService;
import org.junit.jupiter.api.Test;

class PasswordSecretPreflightCheckTest {

@Test
void testEmptyDB() {
final String passwordSecret = RandomStringUtils.secure().nextAlphanumeric(20);
final InMemoryClusterConfigService clusterConfigService = new InMemoryClusterConfigService();
final PasswordSecretPreflightCheck preflightCheck = createCheckInstance(passwordSecret, clusterConfigService);
preflightCheck.runCheck();
}

@Test
void testSuccessfulValidation() {
final String passwordSecret = RandomStringUtils.secure().nextAlphanumeric(20);
final InMemoryClusterConfigService clusterConfigService = new InMemoryClusterConfigService();
final PasswordSecretPreflightCheck preflightCheck = createCheckInstance(passwordSecret, clusterConfigService);

// first check will persist the value in the DB
preflightCheck.runCheck();

// second should read it from there and validate
preflightCheck.runCheck();
}

@Test
void testFailingValidation() {
final RandomStringUtils randomStringUtils = RandomStringUtils.secure();
// first check will persist the value in the DB
final InMemoryClusterConfigService clusterConfigService = new InMemoryClusterConfigService();
createCheckInstance(randomStringUtils.nextAlphanumeric(20), clusterConfigService).runCheck();

Assertions.assertThatThrownBy(() -> {
// now repeat the check, but with different password. Should fail
createCheckInstance(randomStringUtils.nextAlphanumeric(20), clusterConfigService).runCheck();
})
.isInstanceOf(PreflightCheckException.class)
.hasMessageContaining("Invalid password_secret");

}

@Nonnull
private static PasswordSecretPreflightCheck createCheckInstance(String passwordSecret, ClusterConfigService clusterConfigService) {
final EncryptedValueService encryptionService = new EncryptedValueService(passwordSecret);
return new PasswordSecretPreflightCheck(clusterConfigService, encryptionService);
}
}
Loading