Fire Engine

消防士→ITエンジニア→研究者

ユーザが接続先を意識しないSSHプロキシサーバを作った

今回は、ユーザが接続先を意識しないSSHプロキシサーバを作った話です。
SSHのユーザ名から動的に接続先ホストを決定し、SSH接続をプロキシします。

github.com

作った背景

比較的規模の大きなサーバ群を管理しており、そこに対して接続してくるユーザに特定のサーバを使ってもらいたい場合を考えます。
すなわち「ユーザtsurubeeには、ssh102サーバを使ってほしい」といったようにユーザとマシン間が紐づいている場合の一番単純な運用方法は、個々のユーザが接続先ホストの情報を知っていることです。
これでも問題ないのですが、何かしらのサーバ管理の理由でユーザに使ってもらいたいサーバが変更した場合、ユーザに通知するなどして意識的に接続先を変更してもらう必要があります。

f:id:hirotsuru314:20180901145409p:plain

このようなユーザとそのユーザに使ってもらいたいサーバの紐付け情報をサーバ管理側が一元的に管理して、ユーザに意識させることなく、ユーザとサーバの紐付けを自由にコントロールすることができないかと考え、sshrというプロキシサーバを作りました。

sshrは何ができるのか

f:id:hirotsuru314:20180901145521p:plain

sshr導入後のSSHの流れは以下の通りです。

  1. SSHクライアントはsshrサーバに対してSSH接続する
  2. sshrサーバは接続してきたSSHユーザ名から接続先ホストを動的に決定する
  3. sshrサーバは特定した接続先ホストに対してSSHをプロキシする

ここで、重要なのは上図のPluggable Hooksの部分です。ここにsshrを利用する開発者が自由にロジックを組み込めるようにしています。
例えば、サーバ数・ユーザ数の規模がそんなに大きくない場合は、tomlファイルのようなもので簡単に管理したいかもしれませんし、規模が大きくなると、ユーザとそのユーザに使ってもらいたいサーバの紐付け情報をDBで管理したい場合もあるでしょう。後者の場合はPluggable Hooksの部分にDBからSELECTする、もしくはAPIサーバ経由でGETするといった処理を書けばよいわけです。

使ってみる

まずはsshrをインストールします。

$ go get github.com/tsurubee/sshr

sshrのトップディレクトリに移動して、docker-compose upを叩くと、sshrサーバが一台、接続先サーバ用としてhost-tsurubee(tsurubeeユーザの接続先)とhost-hogehogeユーザの接続先)のコンテナが立ち上がります。

$ docker-compose up
Creating sshr_host-hoge_1     ... done
Creating sshr_host-tsurubee_1 ... done
Creating sshr_ssh-proxy_1     ... done
Attaching to sshr_host-tsurubee_1, sshr_host-hoge_1, sshr_ssh-proxy_1
ssh-proxy_1      | ==> Installing Dependencies
host-tsurubee_1  | Starting crond: [  OK  ]
host-hoge_1      | Starting crond: [  OK  ]
ssh-proxy_1      | go get -u github.com/golang/dep/...
ssh-proxy_1      | dep ensure
ssh-proxy_1      | go run main.go
ssh-proxy_1      | time="2018-09-01T07:15:03Z" level=info msg="Start Listening on [::]:2222"

sshrサーバはローカルホストのポート2222でListenしているため、まずはtsurubeeユーザで接続してみます。パスワードはtestとしています。

$ ssh tsurubee@127.0.0.1 -p 2222
tsurubee@127.0.0.1's password:
[tsurubee@host-tsurubee ~]$ hostname
host-tsurubee
[tsurubee@host-tsurubee ~]$ ls /
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  sbin  selinux  srv  sys  tmp  usr  var

すると、host-tsurubeeSSHログインできていることがわかります。
ログイン後は通常SSHでリモートサーバに繋いだ時と同じように対話的にコマンドが実行できます。
次にhogeユーザで繋いでみると、host-hogeに繋がっていることがわかります。(こちらもパスワードはtest)

$ ssh hoge@127.0.0.1 -p 2222
hoge@127.0.0.1's password:
[hoge@host-hoge ~]$ hostname
host-hoge

ポイントは、クライアントはユーザ名以外は同じで

$ ssh ユーザ名@127.0.0.1 -p 2222

と接続しているが、sshrによって接続先が動的に振り分けられている点です。

ちなみに、SCPもできます。

$ scp -P 2222 main.go tsurubee@127.0.0.1:
tsurubee@127.0.0.1's password:
main.go                         100%  853   424.8KB/s   00:00

$ ssh tsurubee@127.0.0.1 -p 2222
tsurubee@127.0.0.1's password:
Last login: Sat Sep  1 07:18:08 2018 from sshr_ssh-proxy_1.sshr_default
[tsurubee@host-tsurubee ~]$ ls -l
合計 4
-rw-r--r-- 1 tsurubee tsurubee 853  9月  1 07:30 2018 main.go

実装について

実装についての補足をいくつか書きます。

SSHプロトコル

今回SSHプロキシサーバを実装するにあたり、当然ですがSSHプロトコルの理解が必要でした。そしてプロトコルを理解するために以下のRFCを読みました。

  • RFC4251:SSH Protocol Architecture
    SSHプロトコル全般(暗号アルゴリズムやキーなど)について規定

  • RFC4252:SSH Authentication Protocol
    ユーザ認証(パスワード認証や公開鍵認証)について規定

  • RFC4253:SSH Transport Layer Protocol
    トランスポート層について規定

  • RFC4254:SSH Connection Protocol
    チャネル制御やポートフォワーディングについて規定

中でも今回の実装で重要だったのが、SSH Transport Layer ProtocolSSH Authentication Protocolです。
sshr自体は認証を通して暗号化されたトランスポート層の確立ができると、あとはL4レベルでのプロキシをするだけの役割になるため、アプリケーションのレイヤーでどのような要求がきているか、などは解釈する必要がありません。(ユーザ名だけは取得する必要はある)
これについて図にすると以下のようなイメージです。

f:id:hirotsuru314:20180901172248p:plain

ユーザ認証

今回の実装で一番苦労したのは間違いなくユーザ認証です。それはなぜかというと、今回のようにクライアントとサーバの間に入ってSSHの仲介をするということは、クライアントやサーバにとっては中間者攻撃のように見えてしまうからです。

ユーザ認証とはサーバがクライアントの正当性を確認するもので、その認証方式にはパスワード認証や公開鍵認証などありますが、パスワード認証の場合、実装は簡単でした。パスワード認証の場合は、sshrサーバは特に意識することなくクライアントから受けた認証時のパケットをそのまま後ろの接続先サーバに対して流し、接続先サーバが返してきたレスポンスをそのままクライアントに返してやる方法で認証が確立できます。

一方、公開鍵認証の場合、クライアントはセッションIDというサーバ間のセッションで固有の乱数に対して、秘密鍵で署名して渡します。これによりお互いがなりすましでなく、接続したい相手であることを確認できます。
しかし、sshrサーバではクライアント・サーバに対して二つのコネクションを有しており、その間に入って意図的に中間者攻撃をしているような状態です。 当然二つのコネクションのセッションIDは異なるため、パスワード認証の時のようにクライアントから受け取ったパケットをそのままサーバ側に流すだけでは、認証ができず、二段階のユーザ認証が必要になってしまいます。(sshrの実装もそうなっています)そうなった場合、authorized_keysの運用をどうするかなどの、運用・管理の問題が出てきそうなので、色々考え中です…

sshpiperについて

sshrと同様にユーザ名ベースでSSHの接続先を切り替えられるSSHプロキシサーバとして、sshpiperという素晴らしい既存のOSSがあり、私も実装の参考にさせていただいています。
sshpiperについては以前の記事でも言及しているのですが、今回こちらのOSSを利用しなかった一番大きな理由は、ユーザ名から接続先ホストを特定する処理がパッケージに組み込まれていて自由に拡張できないという点です。そのため、私のsshrでは、前述のPluggable Hooksの部分で、自由にロジックを組み込めるようにしています。
フック以外の部分でも、より汎用性の高いOSSとして追加で機能を実装していくつもりです!

今後やりたいこと

  • テストを書く
    テストを実装し、CIで自動テストする。

  • より汎用性を高める
    現在の実装だと所々でハードコーティングしてしまっている部分があるため、そこをConfigに寄せるなり、Middleware層に寄せてフックをかけるようにしたりなどすることでsshrの汎用性を高めていきたいと思っています。