今回は、ユーザが接続先を意識しないSSHプロキシサーバを作った話です。
SSHのユーザ名から動的に接続先ホストを決定し、SSH接続をプロキシします。
作った背景
比較的規模の大きなサーバ群を管理しており、そこに対して接続してくるユーザに特定のサーバを使ってもらいたい場合を考えます。
すなわち「ユーザtsurubeeには、ssh102サーバを使ってほしい」といったようにユーザとマシン間が紐づいている場合の一番単純な運用方法は、個々のユーザが接続先ホストの情報を知っていることです。
これでも問題ないのですが、何かしらのサーバ管理の理由でユーザに使ってもらいたいサーバが変更した場合、ユーザに通知するなどして意識的に接続先を変更してもらう必要があります。
このようなユーザとそのユーザに使ってもらいたいサーバの紐付け情報をサーバ管理側が一元的に管理して、ユーザに意識させることなく、ユーザとサーバの紐付けを自由にコントロールすることができないかと考え、sshrというプロキシサーバを作りました。
sshrは何ができるのか
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-hoge
(hogeユーザの接続先)のコンテナが立ち上がります。
$ 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-tsurubee
にSSHログインできていることがわかります。
ログイン後は通常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
ユーザ認証(パスワード認証や公開鍵認証)について規定
中でも今回の実装で重要だったのが、SSH Transport Layer Protocol
とSSH Authentication Protocol
です。
sshr自体は認証を通して暗号化されたトランスポート層の確立ができると、あとはL4レベルでのプロキシをするだけの役割になるため、アプリケーションのレイヤーでどのような要求がきているか、などは解釈する必要がありません。(ユーザ名だけは取得する必要はある)
これについて図にすると以下のようなイメージです。
ユーザ認証
今回の実装で一番苦労したのは間違いなくユーザ認証です。それはなぜかというと、今回のようにクライアントとサーバの間に入ってSSHの仲介をするということは、クライアントやサーバにとっては中間者攻撃のように見えてしまうからです。
ユーザ認証とはサーバがクライアントの正当性を確認するもので、その認証方式にはパスワード認証や公開鍵認証などありますが、パスワード認証の場合、実装は簡単でした。パスワード認証の場合は、sshrサーバは特に意識することなくクライアントから受けた認証時のパケットをそのまま後ろの接続先サーバに対して流し、接続先サーバが返してきたレスポンスをそのままクライアントに返してやる方法で認証が確立できます。
一方、公開鍵認証の場合、クライアントはセッションIDというサーバ間のセッションで固有の乱数に対して、秘密鍵で署名して渡します。これによりお互いがなりすましでなく、接続したい相手であることを確認できます。
しかし、sshrサーバではクライアント・サーバに対して二つのコネクションを有しており、その間に入って意図的に中間者攻撃をしているような状態です。 当然二つのコネクションのセッションIDは異なるため、パスワード認証の時のようにクライアントから受け取ったパケットをそのままサーバ側に流すだけでは、認証ができず、二段階のユーザ認証が必要になってしまいます。(sshrの実装もそうなっています)そうなった場合、authorized_keys
の運用をどうするかなどの、運用・管理の問題が出てきそうなので、色々考え中です…
sshpiperについて
sshrと同様にユーザ名ベースでSSHの接続先を切り替えられるSSHプロキシサーバとして、sshpiperという素晴らしい既存のOSSがあり、私も実装の参考にさせていただいています。
sshpiperについては以前の記事でも言及しているのですが、今回こちらのOSSを利用しなかった一番大きな理由は、ユーザ名から接続先ホストを特定する処理がパッケージに組み込まれていて自由に拡張できないという点です。そのため、私のsshrでは、前述のPluggable Hooks
の部分で、自由にロジックを組み込めるようにしています。
フック以外の部分でも、より汎用性の高いOSSとして追加で機能を実装していくつもりです!
今後やりたいこと
テストを書く
テストを実装し、CIで自動テストする。より汎用性を高める
現在の実装だと所々でハードコーティングしてしまっている部分があるため、そこをConfigに寄せるなり、Middleware層に寄せてフックをかけるようにしたりなどすることでsshrの汎用性を高めていきたいと思っています。