How to do python SSH tunelling with 2FA?

Viewed 1487

I am trying to write a python script that connects to a database, but that database is only accessible if you first SSH to a bastion host. From the command-line, this is what I have to do to setup the tunnel:

$ ssh -i /path/to/my/key/file.pem -A -L 3307:mydb1.fsd23rqr.us-east-1.rds.amazonaws.com:3306 my_ssh_username1@my-bastion-host.without.2fa
[ec2-user@ip-X-X-X-X ~]$ 

I have created a python script using this sshtunnel library to do the same thing. It works:

#!/usr/bin/env python
from sshtunnel import SSHTunnelForwarder
import MySQLdb as db
import pandas as pd

print('\nAbout to try connecting')
with SSHTunnelForwarder(
    ('my-ssh-host.without.2fa', 22),
    ssh_username='my_ssh_username1',
    ssh_pkey='/path/to/my/key/file.pem',
    remote_bind_address=('mydb1.fsd23rqr.us-east-1.rds.amazonaws.com', 3306),
    local_bind_address=('0.0.0.0', 3307)
) as tunnel:
    print "Connection Established"
    print pd.read_sql_query(
        "select 'Hello' from dual",
        db.connect(
            host='127.0.0.1',
            port=tunnel.local_bind_port,
            user='my_db_username1',
            passwd='****************',
            db='my_db_instance_1'
        )
    )

Produces:

About to try connecting
Connection Established
   Hello
0  Hello

But now I need to use a different DB and bastion combination. This bastion has 2-factor-authentication (2FA) setup so that when I ssh to it, It asks me which method of 2FA I choose. If I choose method #1, it sends a push notification to my android phone which I have to approve:

$ ssh -i /path/to/my/key/rsa -A -L 3307:mydb2.fsd23rqr.us-east-1.rds.amazonaws.com:3306 my_ssh_username2@my-bash-host.with.2fa
Duo two-factor login for my_ssh_username2

Enter a passcode or select one of the following options:

 1. Duo Push to XXX-XXX-9671
 2. Phone call to XXX-XXX-9671
 3. SMS passcodes to XXX-XXX-9671 (next code starts with: 1)


Passcode or option (1-3): 1

<WAIT FOR ME TO CLICK APPROVE ON MY ANDROID>

Success. Logging you in...
[my_ssh_username2@ip-X-X-X-1X ~]$

But when I try to setup the tunnel using sshtunnel the exact same way to the 2FA-enabled bastion it fails:

About to try connecting
Connection Established
Enter DB password:
2018-08-29 15:31:43,985| ERROR   | Could not establish connection from ('127.0.0.1', 3307) to remote side of the tunnel
Traceback (most recent call last):
  <stack-trace snipped>
  File "python2.7/site-packages/MySQLdb/__init__.py", line 81, in Connect
    return Connection(*args, **kwargs)
  File "python2.7/site-packages/MySQLdb/connections.py", line 193, in __init__
    super(Connection, self).__init__(*args, **kwargs2)
_mysql_exceptions.OperationalError: (2013, "Lost connection to MySQL server at 'reading initial communication packet', system error: 0")

How can I write a python script to connect to a Mysql DB through an SSH-tunnel if sshing to that host requires 2FA??

1 Answers

You need to setup an interactive SSH session in order to input the options provided by the 2fa enabled server. Having a brief look at the code for the sshtunnel library - it seems like they use the paramiko library to connect to the gateway as well as setup a tunnel. The methods implementing both of these do not have an option for providing an interactive tty for user input.

For example, the method performing gateway connection says this:

def _connect_to_gateway(self):
    Open connection to SSH gateway
     - First try with all keys loaded from an SSH agent (if allowed)
     - Then with those passed directly or read from ~/.ssh/config
     - As last resort, try with a provided password

As you can see, there is no option to perform interactive ssh session commands here.

So your options are to either extend the code using paramiko to set up an interactive session, or write your own code using paramiko to manage both the gateway connection and tunnel setup. For example paramiko provide this example to perform interactive sessions.

Related