Using MongoDB connection via SSH tunnel fails with 'AuthenticationFailed'

Hey everyone,

I have some troubles with connecting to one of our Mongo DB instances from my local machine via Python. While I seem to be able to connect, I cannot use the connection and all actions result in an authentication error.

  • The exact same MongoDB URI string and SSH settings I am using in Python are working fine in MongoDB Compass.
  • The remote is on Azure.
  • The error I am getting is Authentication failed., full error: {'ok': 0.0, 'errmsg': 'Authentication failed.', 'code': 18, 'codeName': 'AuthenticationFailed'}
  • I am using a MongoDB URI in the form "mongodb://{user}:{password}@127.0.0.1:27017/{collection}", I also tried making the authentication source explicit as explained here ("mongodb://{user}:{password}@127.0.0.1:27017/{collection}?authSource=admin") with no luck.
  • And yes, the mongo db user I am logging in with, has both access rights to the /admin and /{collection} collections.

Any hints as to what I could be doing wrong?

Cheers,
Ferdinand

My code:

from sshtunnel import SSHTunnelForwarder
from pymongo import MongoClient

class MongoDatabaseSshConnection:
    """
    """
    def __init__(self, server_url: str, server_port: int, server_user: str, server_password: str, 
                 database_uri: str, binding: tuple[str, int] = ("127.0.0.1", 27017), 
                 autoConnect: bool = True) -> None:
        """
        """
        self.mongoClient: MongoClient | None = None
        self.sshTunnel: SSHTunnelForwarder | None = None

        self.server_user: str = server_user
        self.server_password: str = server_password 
        self.server_port: int = server_port
        self.server_url: str = server_url
        self.database_uri: str = database_uri
        self.binding: tuple[str, int] = binding

        if autoConnect:
            self.Connect()

    
    def Connect(self) -> None:
        """
        """
        self.tunnel = SSHTunnelForwarder(
            (self.server_url, self.server_port),
            ssh_username=self.server_user,
            ssh_password=self.server_password,
            remote_bind_address=self.binding
        )
        self.tunnel.start()
        self.client = MongoClient(self.database_uri)

    def __del__(self) -> None:
        """
        """
        if self.client is not None:
            self.client.close()
        if self.tunnel is not None:
            self.tunnel.stop()
    
    def TestRemoteConnection(self) -> None:
        """
        """
        try:
            self.client.admin.command('ping')
            print("Ping successful")
        except Exception as e:
            print(f"Ping failed: {e}")


def main() -> None:
    """
    """
    serverUrl: str
    serverPort: int
    userName: str
    userPassword: str
    databaseUri: str
    binding: tuple[str, int] = ("127.0.0.1", 27017)

    connection: MongoDatabaseSshConnection = MongoDatabaseSshConnection(
        serverUrl, serverPort, userName, userPassword, databaseUri, binding)
    
    connection.TestRemoteConnection()

Hi,

Have you tried passing directConnection=True to your MongoClient, as described in the docs here?

How did you create the user?
Making a SSH connection to localhost, i.e. to the current machine looks a bit useless to me.

Hey,

thank your for your replies. A simple:

def TestRemoteConnection(self) -> None:
    """
    """
    print(f"{self.mongoClient.server_info() = }")

will also raise the error. And I of course misspoke in my prior posting, where I talked about collections, I meant databases.

Have you tried passing directConnection=True to your MongoClient, as described in the docs here?

Thank you for the tip. No, I did not, but it does not seem to make a difference. I updated the code snippet, the error remains the same.

How did you create the user? Making a SSH connection to localhost, i.e. to the current machine looks a bit useless to me.

How do you mean that and which user do you mean (because there is a MongoDB user and an Azure user)? I am trying to tunnel to a MongoDB instance on an Azure remote. I am not showing the credentials in my snippet for obvious reasons. The Azure server and MongoDB database on it and their users are in use for quite some time and work. All these credentials also work when using the Compass app, and using the SSH option there in the connections. New is only that I want to do work from my local machine with Python on that remote (instead of having to run scripts on that Linux VM directly).

The self.binding is the (local, port) bundle from which the tunnel is forwarding. I.e., the MongoClient client would send there its commands, and the tunnel then forwards them to the remote. At least that is how I meant to set it up. There is pymongo-ssh and it is set up quite similarly. I would like to avoid having dependencies/external code, but I will probably give it a spin tomorrow.

My hunch is that some of our stupid enterprise firewall/VPN security measures is interfering here somehow. But I already turned off what I can turn off with no luck. The other thing which could be an issue, is that the forwarding somehow fails because I have also a MongoDB installation on my local machine, and its service then collides with the tunnel, as it listens on that same standard MongoDB port. But the Compass app works, but maybe it is one of the cases where things are much more complex than they seem. Will probably either have to kill the local MongoDB service tomorrow or try to use a different port.

Cheers,
Ferdinand

My current code:

from sshtunnel import SSHTunnelForwarder
from pymongo import MongoClient

class MongoDatabaseSshConnection:
    """
    """
    def __init__(self, server_url: str, server_port: int, server_user: str, server_password: str, 
                 database_uri: str, binding: tuple[str, int] = ("127.0.0.1", 27017), 
                 autoConnect: bool = True) -> None:
        """
        """
        self.mongoClient: MongoClient | None = None
        self.sshTunnel: SSHTunnelForwarder | None = None

        self.server_user: str = server_user
        self.server_password: str = server_password 
        self.server_port: int = server_port
        self.server_url: str = server_url
        self.database_uri: str = database_uri
        self.binding: tuple[str, int] = binding

        if autoConnect:
            self.Connect()

    
    def Connect(self) -> None:
        """
        """
        self.sshTunnel = SSHTunnelForwarder(
            (self.server_url, self.server_port),
            ssh_username=self.server_user,
            ssh_password=self.server_password,
            remote_bind_address=self.binding
        )
        self.sshTunnel.start()
        self.mongoClient = MongoClient(self.database_uri, directConnection=True)

    def __del__(self) -> None:
        """
        """
        if self.mongoClient is not None:
            self.mongoClient.close()
        if self.sshTunnel is not None:
            self.sshTunnel.stop()
    
    def TestRemoteConnection(self) -> None:
        """
        """
        print(f"{self.mongoClient.server_info() = }")
        # try:
        #     self.mongoClient.nodebb.command('ping')
        #     print("Ping successful")
        # except Exception as e:
        #     print(f"Ping failed: {e}")

def main() -> None:
    """
    """
    ...

Okay, it was my locally running MongoDB instance which caused the issues. The Compass app must be doing something more clever, because there connecting with the same credentials, i.e., tunneling over the same ports worked without me manually killing the MongoDB service.

So, for future readers: When trying to tunnel into a remote MongoDB database via Python’s sshtunnel, make sure your local MongoDB instance is shut down when trying to connect, or use different ports.

Cheers,
Ferdinand

According to your code, you connect to IP 127.0.0.1 - which is your local machine. IP 127.0.0.1 (or more precisely any address from 127.0.0.1 to 127.255.255.254) makes a connection to your own computer and definitely not to any remote computer, unless you configured some port forwarding on your machine. But still, it would no make much sense, because when you are on the local machine then you could connect directly to the forwarded host.

You write “I have also a MongoDB installation on my local machine”. When you connect to mongodb://{user}:{password}@127.0.0.1:27017/, then you are exactly connecting to this instance.

Hey Wilfried,

Thanks for trying to help me. But the issue is already solved and you are also wrong I would say. My script is forwarding localhost:27017 to a remote. So, that the target in the MongoDB URI is local host is intentional, because as far as the Mongo driver is concerned, you are communcating with a local database, in reality all traffic to that local port will be forwarded to a remote, where then a database listens to that port.

This is a common technique and you can also use this in the Compass app via Edit Connection > Advanced Connection Options > Proxy/SSH and there pick there the authentifcation model which fits your needs. And there you will then also use such MongoDB URI that implies you communciate with a database on the local host but in reality work with a database on a remote.

Cheers,
Ferdinand

As far a I know, only one process can listen to a port. So, either your local MonogDB is listening to port 27017 or the port forwarding process is listening to it.

Anyway, it does not make much sense to argue about this topic. From network point of view, the connection is working, since you get a reply from the MongoDB (however, you may connect to a different MongoDB instance than you may think about)

When you get a Authentication failed. then there are not so many possibilities.

  • Check your password. Does it contain any special characters like $ : / ? # [ ] @? Then you must encode it with urllib.parse.quote
  • In which database did you create the user? Database admin is most common but you can do it in any other database. You can check it with db.getSiblingDB('admin').users.find() The authSource must match this database.