Per Project SSH Keys FTW


by Ara T. Howard

With Rails + Capistrano deployments this setting


ssh_options[:forward_agent] = true

is pretty much the de-facto and sane default. It uses all your ssh key goodness for all commands, making most remote actions 'Just Work '

However, there are times when this can be confusing, for instance, when you have two keys loaded in ssh-agent that map to different github or bitbucket accounts. Which account you get will be a fun little game to figure out ;-/

The following very simple strategy lets you check in per project ssh keys in a very natural way, and then to have capistrano respect those keys for all 'cap XXX' operations.

First, you'll want to create an .ssh/ directory in your project, them dump any relevant keys in there. You can have many pairs, just be sure they are named using the normal *.id_rsa convention.


a:~/git/dojo4/rails_app_m $ ls .ssh
id_rsa.dojo4            id_rsa.dojo4.pub

Next you'll want something like this in your Capfile, which detects these local keys, if present, and uses them via a temporary ssh-agent - otherwise falling back to your global ssh-agent loaded keys.


# if this project has an .ssh/ directory, use those keys for deployment.
# otherwise use the global ones.  local keys are utilized by firing up
# a temporary ssh-agent and adding those local keys to it.
#
# likely you'd have a key(s) that allowed access to a particular host and repo
# (github, bitbucket, etc) in your local .ssh directory.
#
# prolly you want to .gitignore .ssh/ !
#
  dot_ssh = File.expand_path("#{ rails_root }/.ssh", __FILE__)

  globs = [
    File.join(dot_ssh, "*id_rsa"),
    File.join(dot_ssh, "id_rsa*"),
  ]

  ssh_keys = [
    ENV['SSH_KEYS'], globs.map{|glob| Dir.glob(glob)}
  ].join(' ').scan(/[^\s]+/).delete_if{|ssh_key| ssh_key =~ /\.pub$/}

  unless ssh_keys.empty?
    require 'session'

    sh = Session::Sh.new
    at_exit{ sh.close }
    sh.execute 'eval `ssh-agent -s -t 3600`'

    ssh_auth_sock = sh.execute('echo $SSH_AUTH_SOCK').first.to_s.strip
    ssh_agent_pid = sh.execute('echo $SSH_AGENT_PID').first.to_s.strip

    ENV['SSH_AUTH_SOCK'] = ssh_auth_sock
    ENV['SSH_AGENT_PID'] = ssh_agent_pid

    ssh_keys.each do |ssh_key|
      if system "SSH_AUTH_SOCK=#{ ssh_auth_sock } ssh-add #{ ssh_key.inspect } >/dev/null 2>&1"
        Say.say("Using ssh-key #{ ssh_key.inspect }", :color => :green)
      else
        Say.say("Cound not add  ssh-key #{ ssh_key.inspect }", :color => :red)
      end
    end
  end

# the only sane thing is to always forward your local, or global, agent
#
  ssh_options[:forward_agent] = true

You'll need the session gem in you Gemfile of course


# file: Gemfile

gem 'session'


And don't forget to .gitignore you fancy new .ssh setup


# file: .gitignore

.ssh/*


There really isn't that much more to it: now you can setup per project ssh keys, knowing any cap deploy xxx commands will use these project local keys, and still be able to pollute your normal ssh-agent to your heart's content.