Fusic Tech Blog

Fusion of Society, IT and Culture

Dockerでsshdを入れてはいけない問題をSSHのプロキシーを作って解決する
2018/10/17

Dockerでsshdを入れてはいけない問題をSSHのプロキシーを作って解決する

なんかテックブログができたらしいので

今回の記事は、ちょうど季節の変わり目なので、放送の開始されたアニメの1話の感想を1行ずつ延々と書こうかと思っていました。 ただ、初回くらいは真面目に書くかと、ネタを探したら意外といいのがあってさくっと実装もできたので、わざわざ技術ブログっぽいことでも書こうかと思います。基本、最近読んだ記事・リファレンス・本の感想文がメインになることの方が多い予定です。

Dockerで sshd についてよく言われるアレ

Dockerでは差し迫った要求がない限り sshd を入れてはいけないとはいいますが、 docker exec は毎回 -ti オプションを付けないといけなかったりでなかなかめんどくさいです。 systemdの運用に慣れている人は、とりあえず systemd 入った仮想マシンとあまり変わらないコンテナ作りがちでそのついでで sshd が入ってることが多い気がします。

GolangだとSSHサーバーが簡単に作れると知る

読書感想文をLTで発表しに行った、Fukuoka.go #12でたまたまSSHのプロキシーを作る話を聞きました:

言語の標準パッケージというわけでもないようですが、どうやらGolangではcrypto/sshという准・標準ライブラリくらいのSSHパッケージがあるらしいです。 パッケージのリファレンスやサンプルをパラっと見た感じ、簡単にSSHのクライアントとサーバーを作れるみたいで、何かできないかと頭の片隅にありました。

dockerと crypto/ssh をくっつける

基本説明書は後で読むタイプなので、元ネタの伝聞で「簡単そう」という印象だけで実装をはじめました。 目標として:

  • Dockerクライアントライブラリで exec で接続する
  • SSHのユーザー名をdockerのコンテナ名に対応させる
  • 起動するシェルは /bin/bash に決め打ち(Alpine Linuxなどは考えない)

を掲げて実装しました。

成果物はGitHubにあげてあります。 最初はリファレンス読んで、それっぽく実装しようかと思ったんですが、サンプルのコードがかなり使い物になりそうだったのでそれをベースに実装する方針でやったらうまくいきました。 数時間程度で、 ssh コマンドでコンテナに exec することができるようになって驚きました。

ちなみに、今回ちゃんと書いた部分はとても少なくて全体で数百行で、更に実際にスクラッチで書いた部分が数十行程度でした。 基本的に、Dockerのクライアントライブラリで取ってきたexecの入出力をsshの入出力とつなげてるだけです:

		exec, err := dockerClient.CreateExec(docker.CreateExecOptions{
			AttachStdin: true,
			AttachStdout: true,
			AttachStderr: true,
			Tty: true,
			Cmd: []string{"/bin/bash"},
			Container: conn.User(),
		})
		if err != nil {
			return err
		}

		err = dockerClient.StartExec(exec.ID, docker.StartExecOptions {
			InputStream: channel,
			OutputStream: channel,
			ErrorStream: channel.Stderr(),
			Tty: true,
			// Detach: true,
			RawTerminal: true,
		})
		if err != nil {
			return err
		}

channelという変数は、多重化された接続の一つを表すssh.Channel型の変数です。新しい多重化された接続が増える度に生成されるため、その度にDockerでシェルを起動します。 ssh.Channelは標準入出力に、そのStderr()メソッドの戻り値は標準エラー出力に対応するのでStartExecに渡す構造体の対応するフィールドにそれぞれ入れてつなげてます。

起動例

Go 1.11のModulesに対応しているのでGo 1.11をインストールしてリポジトリルート下で:

go build && ./go-ssh-docker-proxy

コマンドを実行するとデフォルトのTCPポート2022で起動します。

そして、起動して適当なDockerコンテナをrunしてその名前をSSHのユーザー名にしてこんな風に起動できます:

SSHのRFCを読む

一通り実装が終わったあと、SSHでどんな内部コマンドが投げられているのかの興味でRFC4254を読みながら、ssh コマンドがどんなリクエストを送っているのか実験してました。 サンプルだと最小限しかハンドリングしていないので、機能によっては新しくハンドラを実装する必要があったりで、今後単発のシェルコマンドを投げる時に必要そうです。

今回、X11の仕様がSSHの仕様に書いてあって驚きました。 X11 Forwardingはたまに使うので、こうゆう風に動いていたのかと感心します。 ここら辺の仕様を理解するとmoshでX forwardingを動かすとかもできそうな気がしてきました。

今後

コードをあげたリポジトリのトラッカーにタスクだけ書き出してます。 自動テスト化なんですが、よく考えるとCIサービスでDockerデーモンが立ち上がるのかとか考えるとイマイチで、こうゆうミドルウェアのテストのめんどくささを感じます。

独創性とか考えると、ssh コマンドでつながるDockerシェルとかやると楽しいのかもしれません。

watanabe

watanabe

技術開発部門所属 一番好きな言語はC++です。