1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby Net::SSHで多段ssh(ポートフォワード/トンネリング)をプログラマブルにやる

Posted at

Goal

  • sshをプログラマブルにやる
  • 踏み台サーバ経由でsshする
  • 秘密鍵をメモリ上におき、ディスクにおかなくていいようにする
  • (おまけ)最後にspecinfra, itamaeをこの方式でつかえるようにする

Motivation

秘密鍵の管理がややこしい。案件が増えるにつれてどんどん増えていく。踏み台サーバの鍵と目的サーバの鍵が異なることもあり、サーバ構築時(ユーザ作成前)と構築後でつかう鍵が異なることもある。鍵を担当者に共有するのもややこしい。

少なくとも自分でコントロールできる案件については、構築に関係する鍵をプログラマブルに管理し、構築自体動的にやりたいと思うようになった。

Mechanism

key_data

ふつうコマンドラインのオプションでは-iなど、鍵のパスを指定するのが一般的だが、Net::SSHには key_data というオプションで、文字列で直接秘密鍵を指定する仕組みがある。 1 これをつかえば、秘密鍵をディスク上に保存することなくsshできる。

require 'net/ssh'

key_data = <<-EOS
-----BEGIN RSA PRIVATE KEY-----
MIIEowxxxxxxxx.....
-----END RSA PRIVATE KEY-----
EOS

Net::SSH.start('x.x.x.x', 'ec2-user',
  port: 22,
  key_data: key_data
) do |ssh|
  ssh.exec!("hostname")
end

# => "ip-y-y-y-y\n"

踏み台サーバごしに key_data をつかえれば今回の要件をクリアできるという寸法。

ProxyCommand

capistranoで踏み台経由のデプロイするときは、今までProxyCommandをつかっていた。capistranoは裏でNet::SSHをつかってるので、Net::SSH::Proxy::Commandをつかってプロキシできる。

今回の多段sshも、はじめこれと同じ要領でできるような気がしていたが、どうもできない。

net-ssh/command.rb at v5.2.0 · net-ssh/net-ssh · GitHub

Net::SSH::Proxy::Commandは引数に渡されたコマンドを、OSコマンドライン上で実行しているにすぎない(IO.popen)。つまり Net::SSH がもっているプログラマブルな鍵管理の仕組み(key_dataなど)を適用できない。しかしコマンドラインのsshでは秘密鍵を直接文字列で渡す仕組みはない。(コマンドライン実行されたものは履歴に残るので仮にできたとしてもやるべきではない)

require 'net/ssh'
require 'net/ssh/proxy/command'

key_data = <<-EOS
-----BEGIN RSA PRIVATE KEY-----
MIIEowxxxxxxxx.....
-----END RSA PRIVATE KEY-----
EOS

Net::SSH.start('x.x.x.x', 'ec2-user',
  port: 22,
  key_data: key_data,
  # ↓のコマンドに踏み台サーバログインのための鍵のパスを指定する必要がある
  # 踏み台へのアクセス時にはkey_dataに指定した鍵はつかわれない
  proxy: Net::SSH::Proxy::Command.new('ssh -W %h:%p metheglin@a.a.a.a -i ~/.ssh/id_rsa'),
) do |ssh|
  ssh.exec!("hostname")
end

# => "ip-y-y-y-y\n"

つまり、ProxyCommandつかうなら必ず秘密鍵がファイルとしてディスク上に配置されていなければいけない。今回の要件ではProxyCommandはつかえないことになる。

net-ssh-gateway

じつはわたしは、多段sshについて理解がとぼしく、軽く混乱状態にあった。ProxyCommandができないとわかると混乱きわまってあきらめかけた。
一度冷静になって、できないわけないと思い直し、歯を食いしばって調べ直した結果以下のgemを見つけた。最近歯医者に行ったからくいしばり力があがってたのかもしれない。

GitHub - net-ssh/net-ssh-gateway: A gateway class for tunneling connections via SSH over a forwarded port

READMEにはactiveに開発されてないとコメントがあるが、中身はすごいシンプルで、214行くらいしかない。2 このgemは、シンプルにローカルフォワード3をおこなっている。適当なポートがはらいだされて4、それがフォワードの窓口となる。プログラマはそのポートについて意識しなくてもいい。

Sample Code

net-ssh-gatewayをつかって、秘密鍵をディスクにおかない多段sshは以下のようにできる。

gem install net-ssh-gateway
require 'net/ssh/gateway'

key_data = <<-EOS
-----BEGIN RSA PRIVATE KEY-----
MIIEowxxxxxxxxxxxx...
-----END RSA PRIVATE KEY-----
EOS

gateway = Net::SSH::Gateway.new('x.x.x.x', 'ec2-user', key_data: key_data)
ssh = gateway.ssh('a.a.a.a', 'ec2-user', 
  key_data: key_data
)

ssh.exec!("hostname; pwd; id")
ssh.close
gateway.close(ssh.transport.port)

Net::SSH::Gateway.newした時点で、踏み台への接続が確立される。
その後 gateway.ssh() するともういっこ接続が確立される。

ここでは踏み台サーバとターゲットサーバで同じkey_dataをつかっているが、別の鍵にすることも可能。

specinfra

specinfraでこのgatewayごしのssh接続をどうつかうか?

specinfraではハックポイントが用意されていて、カスタムのssh接続をつかえるようになっていた。5
上記のコード gateway.ssh() でつくった接続 ssh を以下のように指定すればOK。

specinfra = Specinfra::Backend::Ssh.new(
  request_pty: true,
  disable_sudo: false,
  ssh: ssh,
  ssh_options: {
    user: 'ec2-user',
  }
)

specinfra.run_command("hostname; pwd; id")

# ssh.close
# gateway.close(ssh.transport.port)

itamae

itamaeはコマンドライン上の実行を主に想定してるっぽいので、大胆なハックができない。
以下のようにプログラマブルで実行用のRunnerをつくることでしのぐ。
上記specinfraのコードで作成した変数specinfraを指定してつかう。

recipe = File.expand_path("recipes/ping.rb", __dir__)
Itamae::InlineRunner.run(
  [recipe],
  specinfra: specinfra,
  node: {test: 1234},
)
module Itamae
  class InlineRunner < Itamae::Runner
    class << self
      def run(recipe_files, options)
        Itamae.logger.info "Starting Itamae... #{options[:dry_run] ? '(dry-run)' : ''}"

        backend = Backend::SshInline.new(options)
        runner = self.new(backend, options)
        runner.load_recipes(recipe_files)
        runner.run

        runner
      end
    end

    private
    def create_node
      return @options[:node] if @options[:node]
      super
    end
  end

  module Backend
    class SshInline < Base
      def initialize(options)
        @options = options
        @backend = options[:specinfra]
        @executed_commands = []
      end

      def disable_sudo?
        !@options[:sudo]
      end
    end
  end
end

Environment

% ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
[2] pry(main)> Net::SSH::Version::STRING
=> "5.2.0"
[3] pry(main)> Net::SSH::Gateway::VERSION
=> "2.0.0"
[4] pry(main)> Specinfra::VERSION
=> "2.82.4"
[5] pry(main)> Itamae::VERSION
=> "1.10.6"
  1. net-ssh/ssh.rb at v5.2.0 · net-ssh/net-ssh · GitHub

  2. net-ssh-gateway/gateway.rb at v1.2.0 · net-ssh/net-ssh-gateway · GitHub

  3. sshポートフォワーディング - Qiita

  4. net-ssh-gateway/gateway.rb at v1.2.0 · net-ssh/net-ssh-gateway · GitHub (自動的にポートの払い出しがおこなわれるが、番号を明示指定することもできる。)

  5. specinfra/ssh.rb at v2.82.4 · mizzy/specinfra · GitHub

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?