简体   繁体   中英

spring-boot-starter-data-mongodb-reactive setting keystore password from application.yml for connecting using X509

In order to connect to mongodb using reactive streams by authenticating as X509 user, mongo db driver forces to set two jvm properties: javax.net.ssl.keyStore javax.net.ssl.keyStorePassword https://mongodb.github.io/mongo-java-driver/4.0/driver-reactive/tutorials/ssl/

I have only been able to set the properties and make it work before application start

System.setProperty("javax.net.ssl.keyStore", "path");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SpringApplication.run(ChgQuerySvcApplication.class, args);

However if I try to set those properties in a class that extends AbstractReactiveMongoConfiguration It doesn't pick up.

@Configuration
public class ReactiveMongoConfiguration extends AbstractReactiveMongoConfiguration {

    @Autowired
    Environment environment;

    @Value("${mypassword}")
    private String keyStorePassword;

    @Override
    public MongoClient reactiveMongoClient() {

        MongoProperties properties = new MongoProperties();
        properties.setDatabase("somdedb");
        String uri = "mongodb+srv://CN=username@clusteraddress/somedb?authSource=%24external&authMechanism=MONGODB-X509&retryWrites=true&w=majority";
        properties.setUri(uri);
        ReactiveMongoClientFactory factory = new ReactiveMongoClientFactory(properties, environment, null);

        System.setProperty("javax.net.ssl.keyStore", "path to key store");
        System.setProperty("javax.net.ssl.keyStorePassword", "password"); // possibly replace with keyStorePassword
        MongoCredential credential = MongoCredential.createMongoX509Credential("CN=username"); // redundant, I know
        MongoClientSettings settings = MongoClientSettings.builder()
                .applyToSslSettings(builder -> builder
                        .applySettings(SslSettings.builder().enabled(true).invalidHostNameAllowed(true).build()))
                .credential(credential).build();
        return factory.createMongoClient(settings);

    }
}

The spring starter dependency(version 2.3.2.RELEASE) I am using:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>

When I try to connect, I get the following exception:

{"@timestamp":"2020-08-07T17:34:18.346-05:00","logger_name":"org.mongodb.driver.client","thread_name":"async-channel-group-0-handler-executor","severity":"ERROR","trace":"","span":"","parent":"","message":"Calling onError threw an exception","stack_trace":"com.mongodb.MongoCommandException: Command failed with error 18 (AuthenticationFailed): 'No verified subject name available from client' on server servername:27017. The full response is {\"operationTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"ok\": 0.0, \"errmsg\": \"No verified subject name available from client\", \"code\": 18, \"codeName\": \"AuthenticationFailed\", \"$clusterTime\": {\"clusterTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"signature\": {\"hash\": {\"$binary\": {\"base64\": \"4IS/JaRasdauyWO9aXVOcaHm2s+3KzKg=\", \"subType\": \"00\"}}, \"keyId\": 123234}}}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\tat com.mongodb.internal.connection.InternalStreamConnection$2$1.onResult(InternalStreamConnection.java:389)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat...\r\n"}
{"@timestamp":"2020-08-07T17:34:18.347-05:00","logger_name":"org.mongodb.driver.client","thread_name":"async-channel-group-0-handler-executor","severity":"ERROR","trace":"","span":"","parent":"","message":"Callback onResult call produced an error","stack_trace":"com.mongodb.MongoCommandException: Command failed with error 18 (AuthenticationFailed): 'No verified subject name available from client' on server servername:27017. The full response is {\"operationTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"ok\": 0.0, \"errmsg\": \"No verified subject name available from client\", \"code\": 18, \"codeName\": \"AuthenticationFailed\", \"$clusterTime\": {\"clusterTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"signature\": {\"hash\": {\"$binary\": {\"base64\": \"4IS/asdJaRuyaWO9XVOcaHm2s+3KzKg=\", \"subType\": \"00\"}}, \"keyId\": 123234}}}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\tat com.mongodb.internal.connection.InternalStreamConnection$2$1.onResult(InternalStreamConnection.java:389)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat...\r\n"}

The reason I am trying to do that is so that I can set the password through spring cloud config instead of hardcoding or passing as JVM Argument. Is there a way to set those properties dynamically?

All options required for x.509 authentication should be specifiable in the connection string in recent drivers. See here for examples in various languages.

  1. Study connection string documentation .
  2. Construct a connection string containing all of the options.
  3. Use this connection string to connect to your deployment using mongo shell. Do not pass any options using command-line arguments, use connection string only.
  4. Use the same connection string with your driver.

To troubleshoot authentication errors, read the server log .

tl;dr

We were able to provide a new SSLContext to MongoDB connection factory using springboot's MongoClientSettingsBuilderCustomizer before creating a MongoDB Connection, essentially overwriting the default SSLContext Available in JVM

Detailed Explanation:

MongoDB's Java driver relies solely on the SSLContext in JRE and thus there is no way to set it through connection string etc. I confirmed this with MongoDB Support. Since we leverage spring-boot-starter-data-mongodb-reactive . We were able to use some of the Customizers provided by spring-boot. Here is how we solved it:

We created a bean of the customizer:


@Configuration
@RequiredArgsConstructor
public class MongoX509CredentialClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer {

    private static final String MONGO_KEY_ENTRY_ALIAS = "mongo-client-key";

    private static final JcaX509CertificateConverter X509_CERTIFICATE_CONVERTER = new JcaX509CertificateConverter();

    private static final JcaPEMKeyConverter PEM_KEY_CONVERTER = new JcaPEMKeyConverter();

    private final MongoX509Properties properties;

    /**
     * Only customizes the {@link SslSettings} for use with X.509 Certificate
     * Authentication
     */
    @Override
    public void customize(Builder clientSettingsBuilder) {
        // @formatter:off
        clientSettingsBuilder
            .applyToSslSettings(builder -> builder
                    .applySettings(SslSettings
                            .builder()
                            .context(sslContext())
                            .enabled(true)
                            .build()))
            .credential(MongoCredential.createMongoX509Credential());
        // @formatter:on
    }

    /**
     * Creates an {@link SSLContext} that can connect to any endpoint exposing a
     * valid well known CA by the JRE. And uses a dynamic array of
     * {@link KeyManager} that contains the {@link X509Certificate} and
     * {@link PrivateKey} configured for use with a MongoDB instance.
     * 
     * @return
     */
    @SneakyThrows
    public SSLContext sslContext() {
        SSLContext sslContext = SSLContext.getInstance(properties.getTlsVersion());
        sslContext.init(keyManagers(x509Certificate(), privateKey()), trustManagers(), null);
        return sslContext;
    }

    /**
     * Creates an array of {@link TrustManager} containing the default set of
     * trusted certificate authorities. This is required to make a TLS connection to
     * the MongoDB instance. If MongoDB is Atlas then the CA is Let's Encrypt and
     * should already be trusted so copy that over to the SSLContext that we are
     * creating.
     * 
     * @return an array of {@link TrustManager} initialized with the default trust
     *         managers.
     */
    @SneakyThrows
    private TrustManager[] trustManagers() {
        TrustManagerFactory defaultTrustManagerFactory = TrustManagerFactory
                .getInstance(TrustManagerFactory.getDefaultAlgorithm());
        // Using null here init the trustManagerFactory with the default trust store.
        defaultTrustManagerFactory.init((KeyStore) null);

        // only need the default trust managers if the CA for mongo is already in the
        // default trust store for the JVM (e.g. via bosh managed trust store or via the
        return defaultTrustManagerFactory.getTrustManagers();
    }

    /**
     * Creates an array of {@link KeyManager} containing the certificate and
     * privateKey provided in a in memory only {@link KeyStore} with alias
     * {@link #MONGO_KEY_ENTRY_ALIAS} used for x509 authentication. This is a dymaic
     * key manager containing the private key and certificate in the store for use
     * by the {@link SSLContext} that this class creates.
     * 
     * @param certificate
     * @param privateKey
     * @return an array of {@link KeyManager} initialized with the in memory
     *         {@link KeyStore}.
     */
    @SneakyThrows
    private KeyManager[] keyManagers(X509Certificate certificate, PrivateKey privateKey) {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null); // You don't need the KeyStore instance to come from a file.
        keyStore.setKeyEntry(MONGO_KEY_ENTRY_ALIAS, privateKey, "".toCharArray(), new Certificate[] { certificate });

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, "".toCharArray());
        return keyManagerFactory.getKeyManagers();
    }

    /**
     * Parses the PEM Encoded <b> mongo.x509.private-key </b> property to a
     * {@link PrivateKey}.
     * 
     * @return
     */
    @SneakyThrows
    private PrivateKey privateKey() {
        try (PEMParser parser = new PEMParser(new StringReader(properties.getPrivateKey()))) {
            return PEM_KEY_CONVERTER.getPrivateKey(PrivateKeyInfo.class.cast(parser.readObject()));
        }
    }

    /**
     * Parses the PEM Encoded <b>mongo.x509.certificate</b> property to
     * {@link X509Certificate}.
     * 
     * @return
     */
    @SneakyThrows
    private X509Certificate x509Certificate() {
        try (PEMParser parser = new PEMParser(new StringReader(properties.getCertificate()))) {
            return X509_CERTIFICATE_CONVERTER.getCertificate(X509CertificateHolder.class.cast(parser.readObject()));
        }
    }

}



Hence the certificate and key was injected through MongoX509Properties which loaded the properties from a credhub. Another key to this was how you parse the certificate. For that we used the dependency:


<!-- Bouncy Castle for parsing PEM encoded private key and certificate -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.65</version>
</dependency>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM