この記事は Go Advent Calendar 2018 の23日目の記事です。
今回は過去に作成したSSH Managerのお話をさせて頂きます。
どんなツールなのか?
ざっくり言うとSSH接続情報を管理するツールになります。
今の所パスワード認証のみ対応しておりますが、ゆくゆくは公開鍵認証にも対応していく予定です。
簡単に使い方ですが、まずはSSH接続先設定を追加し、
以下のように設定したSSH設定の名前を指定して、実行することでSSH接続が可能になります。
何故作ったか?
所属している会社で運用しているサーバが、パスワード認証で管理されており、毎回user/passwordを入力するのが面倒だなと思ったのがキッカケでした。
パスワード認証の場合、 .ssh/config
での接続管理も出来ないので、だったらサクッとGoでSSH Managerを作ってしまおうと思い立ったのです。
類似のツールがあれば良かったのですが、パスワード認証に対応したツールは見つからず・・・
実装
まずは、実装したいものを大きく3つのステップに分けてみました。
それではひとつずつ実装を追って行きたいと思います。
接続名、接続先、ユーザー、パスワードの追加・更新・削除、及びAES暗号化
実装のイメージとして、下図のようなフローを考えつつ実装を始めました。
順に流れを追って解説致します。
接続情報管理ファイルの操作
接続情報を保持しておくために、接続情報管理ファイルを置き、そこから接続情報の読み込み等を行うようにしました。
そのため、まずは接続情報管理ファイルの存在を確認し、存在しない場合は生成する処理を実装します。
次に、ファイルの存在を確認できたら読み込み処理に入ります。
読み込み処理は少し複雑で、暗号化された接続情報管理ファイルの復号処理を行っております。
今回はAES暗号を採用しており、ハッシュ化にはSHA-256を用いています。
主に復号は次のステップで行っています。
- 暗号・復号に用いる鍵をSHA-256で変換
- 暗号化された接続情報管理ファイルの内容をbase64デコードする
- 変換された鍵を基にブロック・サイファーでのブロックを生成
- CTRモードでの鍵ストリーム生成のために初期化ベクトル、また初期化ベクトルを除いた暗号データを取得
- CTRモードで鍵ストリームを生成し、XORすることで平文を取得
あまり暗号には詳しくないのですが、多分この流れでキチンと暗号化されている・・・はずです。(※有識者の方、ご指摘あればお願いします)
この暗号・復号に際して、deeeetさんのこちらの記事がとても参考になりました。この場をお借りして感謝🙇
さて、無事に復号出来たところで、このデータを用いて接続情報の追加・更新・削除を行っていきます。
まずは追加処理から。
こちらも復号と同じプロセスで接続情報を暗号化し、接続情報管理ファイルに書き込みを行っております。
接続情報入力に際してはサクッと実装したかったのでprompterを使っています。便利。
更新処理はあまり追加処理と変わらないですが、このようになります。
どんどんいきましょう、次は削除処理。
これも指定した接続情報を除外した接続情報配列をファイルに保存し直すという簡単な処理になっています。
この他に、接続情報の一覧表示もあるのですが、説明を省略させていただきます。
パスワード認証によるSSH接続
さて、ここからは保存した接続情報を用いた、パスワード認証によるSSH接続の実装に入ります。
何はともあれ、まずはコードを見ていきましょう。
ガガガっと書かれていますが、こちらも大きく分けて2つの要素で構築されています。
SSHセッションの確立と設定
connect
関数でセッションの確立を行っています。
パスワード認証の場合、SSHクライアント設定にてパスワード認証を渡す必要があります。
セッション確立の中でアレ?となるのは HostKeyCallback: ssh.InsecureIgnoreHostKey()
のところでしょうかね。
これはSSHセッション確立の際に、finger print verificationをskipしている処理になります。
コメントを読むと It should not be used for production code.
と書いてあるんですが、正直使ってるの自分くらいなので怒られたら直します。(あかん
セッションの設定については、入出力の設定とSSH擬似端末の関連付けているところくらいですかね。
あまり特筆すべきことはないですが、入出力設定とかはマストだと思うので書くのを忘れないようにしましょう。
SSH擬似端末の設定
次にSSH擬似端末の設定ですが、ここでいちばん大事なのは擬似端末のモード設定になります。
擬似端末にはRAWモードとCOOKEDモードの2種類があり、何が違うかというと「入力文字データをすぐに送信するか、バッファに溜めてから送信するか」の違いになります。
通常、今回のようなSSHに関するツールを作る際にはRAWモードが使われると思いますが、うっかりCOOKEDモードで動かしてしまうとこんな感じになってしまいます。
気を付けましょう。以下のリンク先を読むとより理解できるかと思います。
また、コード内にコメントとして詳しく書いてありますが、RFC4254を読むことでどのようなパラメータを設定すればよいか?などが分かるので、困った時は読むのをオススメします。
bash-completionの対応
最後にbash-completionの対応に入ります。
bash-completionとはよくあるコマンドの入力補完ですね。今回はサブコマンドや接続名の補完に使っていきたいと思います。
このSSH Managerでは urfave/cli
を使っており、それを前提としたお話になりますのであしからず。
さて、bash-completionの実現には次の2つのステップを踏む必要があります。
- bash-completion機能の有効化
- 接続名補完の実装
bash-completion機能の有効化
urfave/cli
のREADMEにも丁寧にbash-completion機能についての説明がありますが、やることとしては
- EnableBashCompletionをtrueに設定
- bash_autocompleteファイルを置く
になります。
EnableBashCompletionをtrueに設定するのはとても簡単で、以下のように行います。
次に、bash_autocompleteファイルの設置ですが、READMEに詳しい方法が書かれています。
https://github.com/urfave/cli#distribution
要するに、「/etc/bash_completion.d/にbash_autocompleteファイルを置く」という手順になります。
簡単は簡単なのですが、ユーザーにとっては一回限りの行動であったとしてもハードルがあるのは否めません。
そこで、この手順をサブコマンドの一つとして組み込むことにしました。
やっていることは単純で、sudo権限でコマンドを実行してもらい、手順通りに/etc/bash_completion.dディレクトにファイルを設置している形になります。
中でも、sudoのチェックの方法が「uidが0か否か」というのを知らなかったので、実装しながらへぇ〜って唸ってました。
さて、ここからは接続名補完の実装のお話です。
幸いなことに、 urfave/cli
ではbash-completion機能を有効化にするだけで、サブコマンド等の補完は実現できます。
ただし、接続名などCLIツールとしての範疇外のところは自分で実装する必要があります。
該当コードとしては以下の部分です。
見て分かるように、やっていることは単純に接続名を出力しているだけですね。
このように補完したい項目をfmt.Printlnで出力してあげるだけで実装できます。よかったですね。
まとめ
このように、様々なハマりどころがあったりしますが、比較的簡単にSSH Managerを作成することが出来ました。
実際に、業務の中で試したりしていますが、今の所なんとか使えるかなーというレベルにはなっています。
ただし、動的な擬似端末サイズの変更などまだまだ実装しきれていないところがありますので、その辺もゆくゆくは実装していきたいと考えています。
以上、長文でしたがお読み頂きありがとうございました。