rubyssh-keysopensshssh-keygendeploy-keys

Run ssh-keygen in Ruby to generate VCS deploy keys?


I run this command to generate valid deploy keys for my private repos:

ssh-keygen -b 2048 -t rsa -C "mystring"

The command prompts me for a path and a password (which I leave empty), and generates to files.

A mykey:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
...
-----END OPENSSH PRIVATE KEY-----

And a mykey.pub:

ssh-rsa AAAAB3NzaC1yc...QDBQl mystring

But how can I run this command in pure Ruby and get the results as strings?

I have tried:

keypair = OpenSSL::PKey::RSA.new 2048
keypair.to_pem
keypair.public_key.to_s

But the files generated do not resemble the one I get with ssh-keygen.

Anyone who knows how to do this?

P.s. I have found the sshkey gem, but not yet tried it, because I would prefer to avoid using gems for this.


Solution

  • Using Kernel Calls to OpenSSH Utilities

    This is one of those things that in my opinion should not be ported directly to Ruby. While the OpenSSH binaries and source are routinely audited, lightly- or rarely-used Ruby gems or FFI wrappers wouldn't get the same level of scrutiny. Instead, you should use the Kernel#system or Kernel#` calls or the %x() subshell literal, depending on your use case.

    Calling External SSH Utilities from Inside Ruby

    For example:

        # Note that 3072 is currently the default size for RSA keys in
        # OpenSSH. Also note that you can pass `-f path/to/keyfile` or
        # `-P ""` for an empty passphrase if you don't use the `-A` flag.
        system %(ssh-keygen -A -b 2048 -t rsa -C "mystring")
    

    Without the -f flag, your RSA keys will be placed into ~/.ssh/id_rsa and ~/.ssh/id_rsa.pub. You can then use standard Ruby methods for reading the files if you really need to, although again I can't think of many reasons why you'd need to do this.

    For example, to read in your public key into a Ruby variable:

    public_key = File.read "#{ENV['HOME']/.ssh/id_rsa.pub"
    

    Using the SSH-Agent

    Note that if you are already inside a running program, your biggest challenge will be using your SSH agent if you're using one, since the environment variables and key material are unlikely to be available at this point. However, you can start an agent and load your key file within your current session if you like. For example:

    ssh_agent = %x(eval 'ssh-agent -s')
    
    # assumes a passwordless key
    system("ssh-add") && %x(ssh-add -l)
    
    # Your agent's SSH_AUTH_SOCK is now exported by your current Ruby
    # environment.
    ENV['SSH_AUTH_SOCK']
    
    # For some reason, SSH_AGENT_PID isn't exported properly. This may be
    # user error on my part. Luckily, you can easily parse it out of the
    # *ssh_agent* variable if needed.
    ENV['SSH_AGENT_PID'] =
      ssh_agent.match(/SSH_AGENT_PID=\d+/).to_s.split(?=).last
    

    Security Note

    If you're running on macOS or a Linux system with keychain installed, you're better off using a password stored in your login keychain or prompted for when starting ssh-agent. Passwordless keys have their place, but there are more-secure options that are just as easy to manage on most modern systems. YMMV based on your exact use case.

    See Also

    There's a net-ssh gem that also contains ssh-agent support. Whether or not this is suitable for your needs or sufficiently audited for your use case is up to you. However, other visitors who don't want to roll their own or call out to external utilities should be aware of this solution, and there are likely to be others as well. Again, your mileage may vary.