Building SSH Services with JRuby and Apache MINA

When I came up with the idea for Gitris I knew that to successfully provide SSH access to hundreds of users I would have to look beyond the tried-and-true OpenSSH sshd. Creating a Linux user for every Gitris user would be inflexible and inefficient. The GitHub guys patched sshd themselves to authenticate SSH connections against their Rails app, but in my humble opinion introducing and maintaining such patches requires a scary amount of diligence from a security standpoint.

I set out looking for an SSH implementation that would be more suitable for embedding in my nascent application. I found three promising candidates: Python’s Twisted Conch, Go’s go.crypto, and Apache MINA SSHD for Java. I love Python, but sometimes Twisted can be downright inscrutable. I didn’t have much experience with either Go or Java, but I decided that I’d try building my application on JRuby and interfacing with MINA via its promising Java interop capabilities.

The bet paid off, and I have a new favorite Ruby implementation. JRuby powers both the Sinatra app and SSH daemon that make up Gitris. They share code and are both backed by the always-wonderful Redis. JRuby takes a little more time than MRI to start up, and the MINA SSH library is somewhat more CPU-hungry than its OpenSSH counterpart, but the performance is still great overall–and more importantly, the flexibility is unmatched.

Now I’ll show you how to embed a MINA SSH server in your own Ruby code. It’s really easy and opens the door to all kinds of cool applications. This is all you have to do:

# First, download a binary distribution from
# http://mina.apache.org/sshd/sshd-070.html
# and copy lib/*.jar to your cwd

# Load all the jar files
require 'java'
Dir['./*.jar'].each {|jar| require jar }

# Import some Java classes
java_import java.util.EnumSet
java_import org.apache.sshd.SshServer
java_import org.apache.sshd.common.compression.CompressionZlib
java_import org.apache.sshd.server.PasswordAuthenticator
java_import org.apache.sshd.server.PublickeyAuthenticator
java_import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
java_import org.apache.sshd.server.shell.ProcessShellFactory

PORT = 5000

class CustomPasswordAuthenticator
    include PasswordAuthenticator

    def authenticate(username, password, session)
        # Your password authentication magic here
        # Return true for auth success and false for failure
        true
    end

end

class CustomPublickeyAuthenticator
    include PublickeyAuthenticator

    def authenticate(username, key, session)
        # Your public key authentication magic here
        # Return true for auth success and false for failure
        true
    end

end

# Set up the server with sensible defaults for ciphers, etc.
sshd = SshServer.setUpDefaultServer

# Set the port
sshd.setPort(PORT)

# Create a new host key or read an existing one
sshd.setKeyPairProvider(SimpleGeneratorHostKeyProvider.new('key.ser'))

# Enable zlib compression (optional, omit to reduce CPU usage)
sshd.setCompressionFactories([CompressionZlib::Factory.new])

# Install your custom authenticator(s)
sshd.setPasswordAuthenticator(CustomPasswordAuthenticator.new)
sshd.setPublickeyAuthenticator(CustomPublickeyAuthenticator.new)

# Set the shell to run in each new session
# The user's name will be available in the $USER environment variable
sshd.setShellFactory(
    ProcessShellFactory.new(
        ['/bin/sh', '-i', '-l'].to_java(:string),
        EnumSet.of(
            ProcessShellFactory::TtyOptions::ONlCr,
            ProcessShellFactory::TtyOptions::ICrNl)))

# Start the server
sshd.start

Cake! Once you plug in your own authentication logic and custom shell, the sky’s the limit.

There’s one more thing that I found a little tricky. If you want to do public key authentication by comparing against keys uploaded by your users or fetched from the GitHub API, formatted like ssh-rsa longstringhere==, you’ll have to convert from the Java representation first. Thanks to StackOverflow for pointing me in the right direction on this one.

require 'net/ssh'

class CustomPublickeyAuthenticator
    include PublickeyAuthenticator

    def authenticate(username, key, session)
        key = String.from_java_bytes(key.getEncoded)
        key = [OpenSSL::PKey::RSA.new(key).to_blob].pack('m0')
        # The key is now a string
        # Your public key authentication magic here
        # Return true for auth success and false for failure
    end

end

Finally, I want to beg GitHub for a fine-grained OAuth scope that grants read-only access to a user’s public keys. For Gitris I wanted to import each GitHub user’s SSH keys into my database and use them for authentication, but the user scope required for this grants read/write access to keys and all other profile information and would scare away many potential users. Not only does requesting this scope misleadingly inform the user that I want to “update their profile” on the OAuth allow/deny screen, but I also have no interest in giving myself the power to insert my own public key into my users' repositories and gain unfettered push access. To work around these security implications, Gitris generates a random SSH username like c71b7189 for every GitHub user and lets the authentication succeed for any public key or password as long as the user’s stated username exists among the randomly generated ones. I put in a support request with GitHub about this and the support engineer was quite sympathetic, so here’s hoping future changes will enable a new class of effortless SSH authentication for GitHub-aware applications.