How do I connect to external MongoDB with tlsCAFile parameter in connection URL?

This question was asked by one of our community members on our Discord:

I’m trying to connect external MongoDB with my self-hosted appsmith instance and this database requires tlsCAFile parameter in the connection URL.

Here are docs: https://www.mongodb.com/docs/mongodb-shell/reference/options/?_ga=2.160182334.2097400755.1651055997-1131642901.1650613955#std-option-mongosh.--tlsCAFile.

But backend does not support this parameter: Connection string contains unsupported option 'tlscafile'. Is there any solution to pass it?

I tried to find a workaround by importing my certificate to keystore like described here: TLS/SSL. But with no success.

Here is the backend error in the log:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
 - Application run failed
java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798)
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:785)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:345)
	at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:143)
	at com.appsmith.server.ServerApplication.main(ServerApplication.java:15)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
	at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
Caused by: com.github.cloudyrock.mongock.exception.MongockException: com.mongodb.MongoSecurityException: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-256, userName='appsmith', source='appsmith', password=<hidden>, mechanismProperties=<hidden>}
	at com.github.cloudyrock.mongock.runner.core.executor.MongockRunnerBase.execute(MongockRunnerBase.java:68)
	at com.github.cloudyrock.spring.v5.MongockSpring5$MongockApplicationRunner.run(MongockSpring5.java:68)
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:795)
	... 12 common frames omitted
Caused by: com.mongodb.MongoSecurityException: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-256, userName='appsmith', source='appsmith', password=<hidden>, mechanismProperties=<hidden>}
	at com.mongodb.internal.connection.SaslAuthenticator.wrapException(SaslAuthenticator.java:271)
	at com.mongodb.internal.connection.SaslAuthenticator$1.run(SaslAuthenticator.java:85)
	at com.mongodb.internal.connection.SaslAuthenticator$1.run(SaslAuthenticator.java:56)
	at com.mongodb.internal.connection.SaslAuthenticator.doAsSubject(SaslAuthenticator.java:278)
	at com.mongodb.internal.connection.SaslAuthenticator.authenticate(SaslAuthenticator.java:56)
	at com.mongodb.internal.connection.DefaultAuthenticator.authenticate(DefaultAuthenticator.java:53)
	at com.mongodb.internal.connection.InternalStreamConnectionInitializer.authenticate(InternalStreamConnectionInitializer.java:168)
	at com.mongodb.internal.connection.InternalStreamConnectionInitializer.initialize(InternalStreamConnectionInitializer.java:63)
	at com.mongodb.internal.connection.InternalStreamConnection.open(InternalStreamConnection.java:144)
	at com.mongodb.internal.connection.UsageTrackingInternalConnection.open(UsageTrackingInternalConnection.java:51)
	at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.open(DefaultConnectionPool.java:431)
	at com.mongodb.internal.connection.DefaultConnectionPool.get(DefaultConnectionPool.java:115)
	at com.mongodb.internal.connection.DefaultConnectionPool.get(DefaultConnectionPool.java:100)
	at com.mongodb.internal.connection.DefaultServer.getConnection(DefaultServer.java:96)
	at com.mongodb.internal.binding.ClusterBinding$ClusterBindingConnectionSource.getConnection(ClusterBinding.java:123)
	at com.mongodb.client.internal.ClientSessionBinding$SessionBindingConnectionSource.getConnection(ClientSessionBinding.java:135)
	at com.mongodb.internal.operation.ListIndexesOperation$1.call(ListIndexesOperation.java:168)
	at com.mongodb.internal.operation.ListIndexesOperation$1.call(ListIndexesOperation.java:165)
	at com.mongodb.internal.operation.OperationHelper.withReadConnectionSource(OperationHelper.java:583)
	at com.mongodb.internal.operation.ListIndexesOperation.execute(ListIndexesOperation.java:165)
	at com.mongodb.internal.operation.ListIndexesOperation.execute(ListIndexesOperation.java:73)
	at com.mongodb.client.internal.MongoClientDelegate$DelegateOperationExecutor.execute(MongoClientDelegate.java:170)
	at com.mongodb.client.internal.MongoIterableImpl.execute(MongoIterableImpl.java:135)
	at com.mongodb.client.internal.MongoIterableImpl.iterator(MongoIterableImpl.java:92)
	at com.mongodb.client.internal.MongoIterableImpl.forEach(MongoIterableImpl.java:121)
	at com.github.cloudyrock.mongock.driver.mongodb.sync.v4.repository.MongoSync4RepositoryBase.getResidualKeys(MongoSync4RepositoryBase.java:77)
	at com.github.cloudyrock.mongock.driver.mongodb.sync.v4.repository.MongoSync4RepositoryBase.isIndexFine(MongoSync4RepositoryBase.java:65)
	at com.github.cloudyrock.mongock.driver.mongodb.sync.v4.repository.MongoSync4RepositoryBase.ensureIndex(MongoSync4RepositoryBase.java:52)
	at com.github.cloudyrock.mongock.driver.mongodb.sync.v4.repository.MongoSync4RepositoryBase.initialize(MongoSync4RepositoryBase.java:43)
	at com.github.cloudyrock.mongock.driver.core.driver.ConnectionDriverBase.initialize(ConnectionDriverBase.java:40)
	at com.github.cloudyrock.mongock.runner.core.executor.MigrationExecutor.initializationAndValidation(MigrationExecutor.java:225)
	at com.github.cloudyrock.spring.v5.core.SpringMigrationExecutor.initializationAndValidation(SpringMigrationExecutor.java:31)
	at com.github.cloudyrock.mongock.runner.core.executor.MigrationExecutor.executeMigration(MigrationExecutor.java:63)
	at com.github.cloudyrock.spring.v5.core.SpringMigrationExecutor.executeMigration(SpringMigrationExecutor.java:37)
	at com.github.cloudyrock.mongock.runner.core.executor.MongockRunnerBase.execute(MongockRunnerBase.java:53)
	... 14 common frames omitted
Caused by: com.mongodb.MongoCommandException: Command failed with error 17 (ProtocolError): 'Attempt to switch database target during SASL authentication.' on server <hidden>:27018. The full response is {"operationTime": {"$timestamp": {"t": 1651078275, "i": 1}}, "ok": 0.0, "errmsg": "Attempt to switch database target during SASL authentication.", "code": 17, "codeName": "ProtocolError", "$clusterTime": {"clusterTime": {"$timestamp": {"t": 1651078275, "i": 1}}, "signature": {"hash": {"$binary": {"base64": "HtLan8Le5vEXaHti35LhXYWiNyc=", "subType": "00"}}, "keyId": 7090933981415211013}}}
	at com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)
	at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:358)
	at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceive(InternalStreamConnection.java:279)
	at com.mongodb.internal.connection.CommandHelper.sendAndReceive(CommandHelper.java:83)
	at com.mongodb.internal.connection.CommandHelper.executeCommand(CommandHelper.java:33)
	at com.mongodb.internal.connection.SaslAuthenticator.sendSaslContinue(SaslAuthenticator.java:231)
	at com.mongodb.internal.connection.SaslAuthenticator.access$200(SaslAuthenticator.java:47)
	at com.mongodb.internal.connection.SaslAuthenticator$1.run(SaslAuthenticator.java:74)
	... 47 common frames omitted
 - Closed connection [connectionId{localValue:6}] to <hidden>:27018 because there was a socket exception raised by this connection.
 - Operator called default onErrorDropped
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.util.concurrent.CancellationException: Disconnected
Caused by: java.util.concurrent.CancellationException: Disconnected
	at reactor.core.publisher.FluxPublish$PublishSubscriber.disconnectAction(FluxPublish.java:314)
	at reactor.core.publisher.FluxPublish$PublishSubscriber.dispose(FluxPublish.java:305)
	at org.springframework.data.redis.connection.lettuce.LettuceReactiveSubscription$State.terminate(LettuceReactiveSubscription.java:288)
	at org.springframework.data.redis.connection.lettuce.LettuceReactiveSubscription.lambda$cancel$6(LettuceReactiveSubscription.java:177)
	at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
	at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:236)
	at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:203)
	at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onComplete(Operators.java:2058)
	at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onComplete(MonoPeekTerminal.java:299)
	at reactor.core.publisher.MonoIgnoreElements$IgnoreElementsSubscriber.onComplete(MonoIgnoreElements.java:89)
	at io.lettuce.core.RedisPublisher$ImmediateSubscriber.onComplete(RedisPublisher.java:896)
	at io.lettuce.core.RedisPublisher$State.onAllDataRead(RedisPublisher.java:698)
	at io.lettuce.core.RedisPublisher$State$3.read(RedisPublisher.java:608)
	at io.lettuce.core.RedisPublisher$State$3.onDataAvailable(RedisPublisher.java:565)
	at io.lettuce.core.RedisPublisher$RedisSubscription.onDataAvailable(RedisPublisher.java:326)
	at io.lettuce.core.RedisPublisher$RedisSubscription.onAllDataRead(RedisPublisher.java:341)
	at io.lettuce.core.RedisPublisher$SubscriptionCommand.doOnComplete(RedisPublisher.java:778)
	at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:65)
	at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:63)
	at io.lettuce.core.pubsub.PubSubCommandHandler.completeCommand(PubSubCommandHandler.java:260)
	at io.lettuce.core.pubsub.PubSubCommandHandler.notifyPushListeners(PubSubCommandHandler.java:220)
	at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:646)
	at io.lettuce.core.pubsub.PubSubCommandHandler.decode(PubSubCommandHandler.java:112)
	at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:598)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
... (11 lines left)

Answer from @sharat87:

Firstly, we need your root cert somewhere MongoDB can see. Name it something nice and relevant like mongodb-root-cert.pem and copy it to the stacks folder.

Secondly, your link points to the command-line option. This configuration can be set in the connection URI as well: https://www.mongodb.com/docs/manual/reference/connection-string/#mongodb-urioption-urioption.tlsCAFile

So, in your stacks/configuration/docker.env file, look for the APPSMITH_MONGODB_URI variable and add this at the end. For example, if you have this:

APPSMITH_MONGODB_URI=mongodb://username:password@localhost:27017/appsmith

Change it to something like:

APPSMITH_MONGODB_URI="mongodb://username:password@localhost:27017/appsmith?tlsCAFile=/appsmith-stacks/mongodb-root-cert.pem"

(Notice that we’ve added double quotes around the value, this is important). Now, restart with docker-compose restart appsmith and it should take affect.

I cannot reproduce a problem with my remote mongo host, because it is not reachable from container, but when i try connect localhost mongo, I have error in logs: Connection string contains unsupported option 'tlscafile'. Database works after it, because it doesn’t actually need a certificate, but it looks like tlsCAFile is not parsed by backend.

If you have a public reachable database, which needs a custom certificate to connect, we could test it.

From docs you sent (https://www.mongodb.com/docs/manual/reference/connection-string/#mongodb-urioption-urioption.tlsCAFile): This option is not supported by all drivers. Refer to the Drivers documentation . I found Java driver docs about tls connections: https://www.mongodb.com/docs/drivers/java/sync/current/fundamentals/connection/tls/#std-label-tls-ssl


Java applications that initiate TLS/SSL requests require access to cryptographic certificates that prove identity for the application itself as well as other applications with which the application interacts. You can configure access to these certificates in your application with the following mechanisms:

The JVM Trust Store and JVM Key Store
A Client-Specific Trust Store and Key Store

Reply from @sharat87:

Appsmith actually doesn’t use the sync driver, it uses the other Java driver called the Reactive driver, which works in an async fashion. But looks like that driver’s docs don’t seem to talk about this. https://www.mongodb.com/docs/drivers/reactive-streams/

So, is your instance working okay now? I don’t have an instance with a custom root cert for TLS, so I couldn’t test it. But if the connection is working fine, I suppose that’s okay right?

Nope It works fine only with localhost mongo instance, it doesn’t work with external database, which needs custom cert. Now I’m trying to import my cert to trustStore and run appsmith with parameters -Djavax.net.ssl.trustStore=<path> -Djavax.net.ssl.trustStorePassword=<password>

Reply from @sharat87:

Ah, I see. Alright, in that case, the following commands might work to install the root CA as a trusted root inside Appsmith’s container.

cp myRootCert.cer stacks/
docker-compose exec appsmith keytool -import -noprompt -trustcacerts -alias customProxy -file /appsmith-stacks/myRootCert.cer -keystore /usr/lib/jvm/java-1.11.0-openjdk-amd64/lib/security/cacerts -storepass changeit
docker-compose restart appsmith

Note that however, since this change is being applied inside the container, we’ll need to do them again if the container is recreated.

thnx! This is the right way, but that’s not all. After several hours of debugging, I discovered a node-js script chech_replica_set.js

(appsmith/check_replica_set.js at f9b28aedbf3199086dad4702f57d9373e7e0d75f · appsmithorg/appsmith · GitHub) that runs on deploy and also uses APPSMITH_MONGODB_URI . So we need both tlsCAFile parameter in APPSMITH_MONGODB_URI and also an imported cert, like you noticed above.