インフィニットループ 技術ブログ

2016年05月16日 (月)

著者 : mizuno_as

ユーザー名ベースで接続先を切り替えるSSHリバースプロキシ「SSH Piper」を使ってみた

こんにちは、mizuno_asです。みなさんの(社|家庭)内にも、たくさんの開発環境やテスト環境が動作していることだと思います。もちろん弊(社|宅)も例外ではありません。
LXDのようなシステムコンテナを使い、複数の開発環境を1台のホストに集約することも、もはや珍しくありませんよね。一般的に、各コンテナにはローカルなIPアドレスが振られ、コンテナ用のブリッジに接続されていることでしょう。このような場合、コンテナホスト上にNGINX等のリバースプロキシを用意して、HTTP接続は名前ベースで各コンテナにプロキシするのが定番です。

Photo by NOAA’s National Ocean ServiceCC BY 2.0

一方、SSHはどうでしょうか。
一番簡単なのは、コンテナホストを踏み台にして、そこから各コンテナに再度SSHを繋ぐことです。これをOpenSSHのProxyCommandを使って

Host app1.example.com
IdentityFile ~/.ssh/id_rsa.app1
ProxyCommand ssh lxdhost.example.com -W 10.1.1.2:22

のように、自動多段接続を可能にしている人も多いと思います。しかしこの方法では、コンテナを利用するアプリケーション開発者に、コンテナホストへのログインを許可しなければなりません。ホストは触らせたくないですし、なにより利用者には、コンテナの中であるということを意識させたくありません。リバースプロキシするHTTPと同様、各自の机上端末から開発環境に直接接続できる(ように見える)ことが理想です。
どうせHTTPのリバースプロキシにNGINXを使うなら、ngx_stream_core_moduleでTCP接続をプロキシする方法も考えられます。たとえば以下は、ホストのTCP:8022ポートへの接続をコンテナのTCP:22へプロキシする例です。

stream {
    server {
        listen 8022;
        proxy_pass 10.1.1.2:22;
    }
}

しかしHTTPと異なりserver_nameが使えないため、DNSベースでの振り分けができません。コンテナごとにユニークなポート番号を設定しなければならず、これはあまり嬉しくありません。
そんな時に見つけたのが、ログインユーザー名ごとにSSHバックエンドを切り替えることができるSSHリバースプロキシ「SSH Piper」です。

Photo by Bill AbbottCC BY-SA 2.0
今回はUbuntu 16.04 LTSへインストールしました。SSH Piperはgo製のソフトウェアですので、golangパッケージをインストールした上で、環境変数GOPATHを設定し、go getします。

$ sudo apt-get install golang
$ mkdir ~/.go
$ export GOPATH=$HOME/.go
$ go get github.com/tg123/sshpiper/sshpiperd

SSH Piperの動作には、コンフィグを収めたディレクトリが必要です。筆者は~/.sshpiperd以下に、ログ用のディレクトリとあわせて作成しました。

$ mkdir -p ~/.sshpiperd/{config,log}

configディレクトリ内には、SSH Piperにログインするユーザー名のディレクトリを作成し、その中にsshpiper_upstreamというテキストファイルを配置します。結果として、ディレクトリ構造は以下のようになります。

.sshpiperd/
├── config
│   ├── user1
│   │   └── sshpiper_upstream
│   ├── user2
│   │   └── sshpiper_upstream
│   └── user3
│       └── sshpiper_upstream
├── log
     └── sshpiperd.log

sshpiper_upstreamには、そのユーザーが最終的に接続するバックエンドのアドレスを

[ユーザー名@]IPアドレス[:ポート番号]

のフォーマットで記述します。たとえばSSH Piperと同じユーザー名で、10.1.1.2のIPアドレスを持つコンテナの22番ポートにプロキシさせたい場合は、単に

10.1.1.2

と書いておくだけでよいです。
SSH Piperのバイナリは

$GOPATH/bin/sshpiperd

です。起動用のラッパースクリプトが

$GOPATH/src/github.com/tg123/sshpiper/sshpiperd/example/showme.sh

にあるので、これを~/binあたりにコピーして、自分の環境に合うように改造してしまいましょう。たとえばSSH Piperが待ち受けるポート番号を変更したい場合は、$SSHPIPERD_BINに-pオプションを追加してみてください。筆者はコンフィグディレクトリとログの出力先を設定しました。

#!/bin/bash
if [ -z $GOPATH ]; then
    export GOPATH=$HOME/.go
fi
SSHPIPERD_BIN="$GOPATH/bin/sshpiperd"
BASEDIR="$HOME/.sshpiperd"
LOGFILE="$BASEDIR/log/sshpiperd.log"
if [ ! -f $BASEDIR/sshpiperd_key ];then
    ssh-keygen -N '' -f $BASEDIR/sshpiperd_key
fi
for u in `find $BASEDIR/config/ -name sshpiper_upstream`; do
    chmod 400 $u
    upstream=`cat $u`
    username=`dirname $u`
    username=`basename $username`
    echo "ssh 127.0.0.1 -p 2222 -l $username # connect to $upstream"
done
$SSHPIPERD_BIN -i $BASEDIR/sshpiperd_key -w $BASEDIR/config --log $LOGFILE

このラッパースクリプトを実行すると、SSH Piperがフォアグラウンドで起動します。この状態でSSH接続がプロキシされるか動作を確認しましょう ((この時点では–logオプションを指定せず、stdoutに吐かせておく方がいいかもしれません。)) 。うまくいったなら/etc/systemd/system/sshpiperd.serviceといったUnitファイルを作成し、システム起動時に自動実行されるようにしておくとよいでしょう。
コンテナごとにユーザー名を変える必要があるので、結局ポート番号を変えるのと同じじゃないか! というツッコミもあるかもしれません。しかしユーザー名とコンテナ名を揃えておけば、ランダムな数字よりも直感的に使えますし、なにより踏み台用のアカウントを発行しなくていいというのはメリットだと思います。
ただし設定のリロード時に既存セッションが切断されてしまうという仕様のため、頻繁にコンテナを増減させる環境には向かないかもしれません。ご注意ください。

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!