Subversion and SSH authentication shenanigans by Ben Artin

The default behavior of Subversion when tunneled over SSH works well for simple cases. I encountered some more complex situations which required digging into advanced SSH features, and built some simple tools that make our Subversion life easier.

Specifying a username for SSH repositories

By default, when you connect to a Subversion repository over SSH, Subversion will assume that the remote username is the same as the local username. When this is not true, you can force a different username to be used. When you check out the repository, use:

svn checkout svn+ssh://svn-username@svn-hostname/svn-path

Subversion will parse the username out of the URL, and pass it through to SSH, which will then prompt you for the password for the correct account.

Avoiding multiple password prompts

If you have a simple SSH repository, you will be asked for your password once at the beginning of a Subversion command. However, if your repository contains externals, you will be asked for your password again for each external that requires it. If you have multiple externals on the same server, you get to enter the same password multiple times.

To work around this you can use SSH session sharing. Session sharing lets multiple SSH connections to the same server share a single network connection. That also causes multiple SSH connections to the same server to share credentials, and therefore you don’t have to reenter your passwords during a single Subversion command.

To set up SSH session sharing, you need to add this to your ~/.ssh/config:

ControlMaster auto
ControlPath ~/.ssh/master-%r@%h:%p

To verify that it works, open an SSH connection in Terminal, then open a new Terminal window and open another SSH connection to the same account as the first one. The second will not ask for your password.

The configuration above enables SSH connection sharing for all SSH connections. If, instead, you want to only enable connection sharing for your Subversion server, put the following at the bottom of your ~/.ssh/config:

Host svn-hostname
ControlMaster auto
ControlPath ~/.ssh/master-%r@%h:%p

Avoiding multiple password prompts — the pair.com users’ version

On pair.com — I imagine this may also be true of some other hosting providers — the above recipe with SSH session sharing works great as long as your repository has fewer than ten externals. Once you get to ten externals, you see a “channel 10: open failed” message and your Subversion command dies.

This is because pair.com limits SSH session sharing to ten simultaneous connections to a given server. Unfortunately, Subversion keeps every SSH connection open until it’s done with all of them. If you have ten externals on the same server, you wind up with eleven simultaneous SSH connections (ten for the externals and one for the root of the repository), and so your SSH and Subversion error out.

The good news is that SSH already has a mechanism to delegate its password prompting to another tool (this is what programs like Fetch use to give a graphical user interface for entering your SFTP password). If you had a tool that handled SSH password prompting, but was also capable of remembering the passwords you gave it and not asking you to reenter them, you would only get asked for your password once.

An SSH password prompter is relaunched for every prompt, so it needs a place to stash passwords during a single Subversion command. An easy way to handle this is to write a tool which creates a password store and then launches Subversion. Using that tool instead of Subversion will provide the password prompter with the password repository it needs to work correctly.

This is what the Subversion replacement looks like:

#!/usr/local/bin/python

from __future__ import with_statement
from subprocess import call
from tempfile import mkdtemp
from os import mkfifo, environ
from os.path import join, expanduser
from sys import argv
from threading import Thread
from pickle import load, dump
from getpass import getpass

request_pipe_path = join(mkdtemp(), "svn-askpass-requests")
response_pipe_path = join(mkdtemp(), "svn-askpass-responses")

mkfifo(request_pipe_path)
mkfifo(response_pipe_path)

args = argv[1:]

def prompt_loop():
    cache = {}

    while True:
        with open(request_pipe_path, "r") as request_pipe:
            hidden, request = load(request_pipe)

        if not request in cache:
            if hidden:
                cache[request] = getpass(request)
            else:
                cache[request] = raw_input(request)

        with open(response_pipe_path, "w") as response_pipe:
            dump(cache[request], response_pipe)

prompter = Thread(target=prompt_loop)
prompter.daemon = True

environ["SSH_ASKPASS"] = expanduser("~/.subversion/svn-ssh-askpass")
environ["SSH_ASKPASS_REQUEST_PIPE"] = request_pipe_path
environ["SSH_ASKPASS_RESPONSE_PIPE"] = response_pipe_path

prompter.start()

exit(call(["/usr/local/bin/svn"] + args))

There isn’t much to it — it creates two pipes, on one of which it reads prompts and on the other of which it provides user responses. Before starting Subversion, it spawns a background thread, from which it listens for prompts. If it receives a prompt that it has already seen, it returns the same answer as before; if it receives a prompt it hasn’t already seen, it presents it to the user and stores the answer for later.

If you weren’t familiar with them before, you may find these interesting:

  • tempfile.mkdtemp: securely creates a temporary directory readable only by its owner
  • getpass.getpass: securely prompts for a password
  • threading.Thread.daemon: if set on a thread, then the thread is automatically killed off when all other threads are done
  • os.path.expanduser: expands ∼ to user’s home path
  • os.environ["SSH_ASKPASS"]: the way to tell SSH to delegate password prompting

This Subversion wrapper sets up SSH to use ~/.subversion/svn-ssh-askpass for password prompting. However, SSH doesn't use the value of SSH_ASKPASS unless a few specific conditions are met, and those conditions are not met when Subversion command line invokes SSH. Therefore, we also need to create a wrapper for SSH, which will set things up so that password prompting is delegated to the prompter, and then we need to tell Subversion to invoke that SSH wrapper instead of SSH.

This is what the wrapper looks like:

#!/usr/local/bin/python
from os import setsid, environ
from subprocess import call
from sys import argv

# We have to invoke SSH from a process with no controlling terminal
# in order to get SSH_ASKPASS to work
setsid()
environ["DISPLAY"] = "0"

exit(call(["/usr/bin/ssh"] + argv[1:]))

If you don’t really get all the details of this, that’s OK. I don’t either; it’s Soviet-era UNIX programming, I just Googled until I pieced it together.

Save that at ~/.subversion/svn-ssh-detach and tell Subversion to use this SSH wrapper by adding this to your ~/.subversion/config:

[tunnels]
ssh = /Users/ben/.subversion/svn-ssh-detach

Sadly, Subversion doesn’t expand ~ in tunnel configuration, so you have to put the full path to your home there.

OK, now we have a Subversion wrapper that can cache passwords, and we have an SSH wrapper that can delegate its password prompting to a custom prompter. All we need to do to close the loop is write a custom prompter that talks to the Subversion wrapper instead of directly prompting you:

#!/usr/local/bin/python

from __future__ import with_statement
from sys import argv
from os import environ
from pickle import load, dump

prompt = argv[1]
secret = False

if "password:" in prompt:
	secret = True

with open(environ["SSH_ASKPASS_REQUEST_PIPE"], "w") as request_pipe:
	dump((secret, prompt), request_pipe)

with open(environ["SSH_ASKPASS_RESPONSE_PIPE"], "r") as response_pipe:
	print load(response_pipe)

And there you have it. To recap:

From there on, invoke your Subversion wrapper instead of svn (or create an alias that runs the wrapper when you type svn).

In other news:

Comments

  • Yeah, you could do all that wrapper business. Or you could just use SSH public key authentication.

    Rodney January 30, 2012
  • Rodney, that assumes you have control over both ends of this situation. There may be a significant investment in svn+ssh with passwords on the far end and you may not have enough leverage to get them to set up a key just for you – even if that is a better choice.

    I’ve also found that the jsvn command that comes with SVNKit Just Works for svn+ssh with a password – it accepts the username and password on the command line and does not prompt. You can also force it to accept the remote site’s certificates and not save the authentication details.

    Joe McMahon October 12, 2012
  • The subversion wrapper gives errors:

    ./psvn: line 3: from: command not found
    ./psvn: line 4: from: command not found
    ./psvn: line 5: from: command not found
    ./psvn: line 6: from: command not found
    ./psvn: line 7: from: command not found
    ./psvn: line 8: from: command not found
    ./psvn: line 9: from: command not found
    ./psvn: line 10: from: command not found
    ./psvn: line 11: from: command not found
    ./psvn: line 13: syntax error near unexpected token `(‘
    ./psvn: line 13: `request_pipe_path = join(mkdtemp(), “svn-askpass-requests”)’

    Any ideas?

    Jedis July 12, 2016
  • My guess is that you have an extraneous newline at the beginning of psvn, which is causing it not to be run as Python code. Make sure that all three executable scripts have nothing else before the line starting with “#!”

    Ben Artin July 12, 2016
  1. Page 1