Python SSH Server for UNIX Systems using Twisted.conch

I can still recall the excitement of the first time I tried to access and administer a remote system using SSH. Accessing my shell at a remote machine securely, being able to do local and remote port forwarding in order to access remote services through encrypted tunnels, X forwarding, secure file transfers using scp or sftp, authentication using public key infrastructure are just a few of the features that justify the excitement of the first time. The only Secure Shell server implementation I had used all that time was the OpenSSH server. Although this is an open-source project, the fact that it is written in C makes it extremely difficult for me to have fun with it by making any modifications in order to implement even simple things like command filtering. This is because I have never programmed in C and do not intend to learn how to do it in the foreseeable future. So what I’ve been looking for today was a server implementation of the SSH2 protocol written in Python. Unfortunately, there is no such project ready for immediate use. I had to hack my own! After several hours of trial and error, having written dozens of sample scripts for testing, I finally created a minimal project, called RapidSSH, in order to demonstrate how to create a fully functional SSH server with just a few lines of Python code by using Twisted.conch, part of the Twisted Framework. Read on…

At first, I tried to experiment with the sshsimpleserver.py example script that exists on the Twisted homepage. But, soon I realized that this script is there just to give an idea about how to use Twisted.conch and also provide a sensible starting point for your own implementations. That script was not even close in having the functionality I had in mind. After spending some time examining the Twisted source code, I found a fantastic, but completely undocumented, module: twisted.conch.unix. Having gained some experience by my experimentation with sshsimpleserver.py I managed to easily put the pieces together and come up with the SSH server implementation as shown below.

But first, let’s install some dependencies. I assume that you will try the code on a machine aimed for testing, so I won’t be using the system’s package manager but instead use easy_install to install the needed Python modules. Other commands like pip could be used as well. If you do not want to mess with your system-wide python installation, just use virtualenv and pip, but I won’t go into the details about how to use these tools in the current document.

Install the needed dependencies:

easy_install pycrypto
easy_install pyasn1
easy_install pam
easy_install twisted

Make sure you have gcc installed, because it will be needed to complete the installation of Twisted and PyCrypto.

Below is a small script I wrote in order to test the basic functionality. Filename: rapidsshd_unix.py.

#!/usr/bin/env python
#
# rapidssh - A Secure Shell (SSH) server implementation in Python
#            using Twisted.conch, part of the Twisted Framework.
#
# Copyright (c) 2010 George Notaras - http://www.g-loaded.eu
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Initially based on the sshsimpleserver.py kindly published by:
# Twisted Matrix Laboratories - http://twistedmatrix.com
#
 
import sys
import pam
 
from twisted.conch.unix import UnixSSHRealm
from twisted.cred import portal
from twisted.cred.credentials import IUsernamePassword
from twisted.cred.checkers import ICredentialsChecker
from twisted.cred.error import UnauthorizedLogin
from twisted.conch.checkers import SSHPublicKeyDatabase
from twisted.conch.ssh import factory, userauth, connection, keys, session
from twisted.internet import reactor, defer
from zope.interface import implements
from twisted.python import log
 
# Logging
# Currently logging to STDERR
log.startLogging(sys.stderr)
 
# Server-side public and private keys. These are the keys found in
# sshsimpleserver.py. Make sure you generate your own using ssh-keygen!
 
publicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHRivcJSkbh/C+BR3utDS555mV'
 
privateKey = """-----BEGIN RSA PRIVATE KEY-----
MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW
4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw
vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb
Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1
xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8
PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2
gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu
DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML
pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP
EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg==
-----END RSA PRIVATE KEY-----"""
 
 
class PamPasswordDatabase:
    """Authentication/authorization backend using the 'login' PAM service"""
    credentialInterfaces = IUsernamePassword,
    implements(ICredentialsChecker)
 
    def requestAvatarId(self, credentials):
        if pam.authenticate(credentials.username, credentials.password):
            return defer.succeed(credentials.username)
        return defer.fail(UnauthorizedLogin("invalid password"))
 
 
class UnixSSHdFactory(factory.SSHFactory):
    publicKeys = {
        'ssh-rsa': keys.Key.fromString(data=publicKey)
    }
    privateKeys = {
        'ssh-rsa': keys.Key.fromString(data=privateKey)
    }
    services = {
        'ssh-userauth': userauth.SSHUserAuthServer,
        'ssh-connection': connection.SSHConnection
    }
 
# Components have already been registered in twisted.conch.unix
 
portal = portal.Portal(UnixSSHRealm())
portal.registerChecker(PamPasswordDatabase())   # Supports PAM
portal.registerChecker(SSHPublicKeyDatabase())  # Supports PKI
UnixSSHdFactory.portal = portal
 
if __name__ == '__main__':
    reactor.listenTCP(5022, UnixSSHdFactory())
    reactor.run()

Some notes:

  • The server uses the very same public and private keys as found in the sshsimpleserver.py script. Make sure you generate new keys using ssh-keygen if you plan to run this on a public server (although this not recommended).
  • Twisted included a module for PAM authentication, twisted.cred.pamauth, but unfortunately I could not locate one of its dependencies to make it work. So, I used the excellent python-pam to create a custom PAM authenticator class.
  • I can guess what you might thought when you read about this code:

    Python is a cross-platform programming language. Twisted runs on Windows. So, is this a solution for running a SSH server on Win32?

    Well, at the moment it won’t run because internally it uses some Python modules that are available on UNIX platforms only. But, I intend to investigate the possibility of running this on Windows since I already need something like that.

We can now enjoy our Python SSH server. Run as root:

python scripts/rapidsshd_unix.py

From another machine, connect to the server using an ssh client:

ssh -p 5022 rocky@arena

If you have deployed your public key in the ~/.ssh/authorized_keys file on the remote machine (arena), you should be able to authenticate using the public key:

ssh -p 5022 -i /path/to/private.key rocky@arena

You should get output like the following on the server:

[root@arena ~]# python ssh.py
2010-03-26 21:30:05+0000 [-] Log opened.
2010-03-26 21:30:05+0000 [-] __main__.UnixSSHdFactory starting on 5022
2010-03-26 21:30:05+0000 [-] Starting factory <__main__.UnixSSHdFactory instance at 0xb7a0b0cc>
2010-03-26 21:30:13+0000 [__main__.UnixSSHdFactory] disabling diffie-hellman-group-exchange because we cannot find moduli file
2010-03-26 21:30:13+0000 [SSHServerTransport,0,192.168.0.172] kex alg, key alg: diffie-hellman-group1-sha1 ssh-rsa
2010-03-26 21:30:13+0000 [SSHServerTransport,0,192.168.0.172] outgoing: aes256-ctr hmac-sha1 none
2010-03-26 21:30:13+0000 [SSHServerTransport,0,192.168.0.172] incoming: aes256-ctr hmac-sha1 none
2010-03-26 21:30:14+0000 [SSHServerTransport,0,192.168.0.172] NEW KEYS
2010-03-26 21:30:14+0000 [SSHServerTransport,0,192.168.0.172] starting service ssh-userauth
2010-03-26 21:30:17+0000 [SSHService ssh-userauth on SSHServerTransport,0,192.168.0.172] rocky trying auth none
2010-03-26 21:30:17+0000 [SSHService ssh-userauth on SSHServerTransport,0,192.168.0.172] rocky trying auth publickey
2010-03-26 21:30:17+0000 [SSHService ssh-userauth on SSHServerTransport,0,192.168.0.172] rocky trying auth publickey
2010-03-26 21:30:17+0000 [SSHService ssh-userauth on SSHServerTransport,0,192.168.0.172] rocky authenticated with publickey
2010-03-26 21:30:17+0000 [SSHService ssh-userauth on SSHServerTransport,0,192.168.0.172] starting service ssh-connection
2010-03-26 21:30:17+0000 [SSHService ssh-connection on SSHServerTransport,0,192.168.0.172] got channel session request
2010-03-26 21:30:17+0000 [SSHChannel session (0) on SSHService ssh-connection on SSHServerTransport,0,192.168.0.172] channel open
2010-03-26 21:30:17+0000 [SSHChannel session (0) on SSHService ssh-connection on SSHServerTransport,0,192.168.0.172] pty request: xterm (24L, 80L, 0L, 0L)
2010-03-26 21:30:17+0000 [SSHChannel session (0) on SSHService ssh-connection on SSHServerTransport,0,192.168.0.172] getting shell

This first release of the script for demonstration purposes. Don’t get fooled by the small amount of code. Information about how to put the pieces together is scarce and it required a lot trial&error and source code reading. The above implementation contains none of the customizations I had in mind. These will be done as soon as I find some time to do so. Until then it will be nice to hear about your experiences or implementations you have worked on.

Python SSH Server for UNIX Systems using Twisted.conch by George Notaras is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Copyright © 2010 - Some Rights Reserved