マルチ・プラットフォームを扱うインフラ屋さんのための連携技術: SSH編

# はじめに
API? REST? なんだそれ、うまいのか？
ここでは、プラットフォームをまたがったとしてもそれらを連携するために使える標準的な技術としてSSHを取り上げます。
最近たまたまプロジェクトでこの辺りを使うことがあったので、備忘録代わりのまとめです。

※ここでいうマルチ・プラットフォームとは、基本的にWindows, Unix/Linux, z/OS(Unix System Service)辺りを想定しています。

# SSH概要
rsh(リモートシェル)のセキュア版。何がセキュアかは置いておいて、まぁまぁよく使われるプロトコルです。リモートのマシンにログインしたり、リモートのシェル・スクリプトを実行したりします。
SSHサーバーを立てておいて、SSHクライアントから接続して利用します。Unix/Linuxだと普通にSSHサーバーが構成されていたりしますし、SSHクライアントも大抵入ってますので、リモートのマシンにSSH経由で接続して操作することはよくありますね。
Windowsからだと、SSHクライアントとしてteratermというソフトを使ってUnixマシンにアクセスするってことをよくやります。

## 接続形態
![image01.gif](https://qiita-image-store.s3.amazonaws.com/0/132331/da8feaaa-5e83-f0a4-c5cd-6403f0a22798.gif)
通常、SSHサーバーを構成すると、22番ポートでListenします。
SSHクライアントからSSHサーバーの22番ポートに対してTCPのコネクション確立要求を発行し、そのコネクションを使って通信を行います。間にFirewallなどがある場合は、22番ポートのTCP通信について許可をするよう穴を開けてもらう必要があります。

# 各プラットフォームのSSH実装

## Unix/Linux
まぁここはいいでしょう。なければopen-sshとか入れてもらえれば、サーバーもインストール/構成は比較的容易にできます。使用するOSの作法で適宜インストール/構成して下さい。

## Windows
WindowsはデフォルトでSSHのサーバーとかクライアントとか入ってませんので、なんらかのソフトウェアをインストールする必要があります。
あんまりWindowsをSSHサーバーにすることは無いので、ここではSSHクライアントとして使用することを想定します。

### シェルでの操作
telnetのようにインタラクティブにコマンド操作を行う使い方を行う場合、teratermというオープンソースを使うことが多いと思います。

### sshコマンドでの操作  <=これ注目！
バッチからリモートのサーバーのシェル・スクリプトを呼び出す、といった連携を行う場合は、teratermのようなCUIインターフェースではなくコマンドを使用する必要があります。
SSH関連コマンドに特化している訳ではありませんが、Unix系の機能をWindows上で使うための仕組みというのがいくつか提供されています。
MinGW + MSYSを使うのが手っ取り早いと思います。(その他有名どころとしてはCygwinとかでもよいと思います)

参考: [MinGW+MSYSインストール メモ](http://y-ishida.hatenablog.com/entry/2015/10/26/102749)

以前はダウンロードして展開するだけだったような気がするのだが、最近はインストーラーが提供されているようですね。
MinGWのbinと、msysのbin をパスに追加すればOKのはず。
(Cドライブ直下にインストールしたとすると、set PATH=%PATH%;C:\MinGW\bin;C:\MinGW\msys\1.0\bin こんな感じ)

## z/OS(USS)
z/OSでもUnix System Service(USS)というUnixライクな環境が稼働します。
z/OS版のOpenSSHがIBM Ported Tool for z/OS (z/OSのライセンスがあればフリーで入手できるツール)に含まれて提供されており、USS上でSSHサーバーを稼働させることができます。

参考: [IBM Ported Tools for z/OS](http://www-03.ibm.com/systems/z/os/zos/features/unix/ported/)

# 公開鍵認証
teratermとかで接続する時は、どうせインタラクティブに操作するので、ログイン時にユーザーID/パスワードを入力して認証してもあまり問題はないのですが、バッチやプログラムからSSH経由でリモートのシェル・スクリプトとか実行したい場合、ユーザーID/パスワードの認証だと色々問題があります。パスワードをコードの中に埋め込まないといけなかったり、コマンド実行の際はパスワードをプロンプトで聞いてくるのでその都度入力しないといけなかったりします。
そこで、公開鍵認証方式を使用すると、いちいちパスワードをセットする必要が無く、システムをまたがった連携/自動化が楽にできます。

ざっくりとした手順としては、キーペア(秘密鍵と公開鍵のペア)を作成し、公開鍵をSSHサーバー側に設定しておきます。秘密鍵を持っているユーザーから公開鍵を設定しているSSHサーバーにアクセスした場合、そのキーペアのマッチングが行われ、アクセスが可能となります。

WindowsをSSHクライアント、Unix/Linux/zOS(USS)をSSHサーバーと想定して設定方法を記載します。
(Unix/Linuxがクライアントになる場合でもやる事は同じです。)

全体像
![image02.gif](https://qiita-image-store.s3.amazonaws.com/0/132331/b75b4b8c-50d9-08da-5152-7761956b9a25.gif)

## SSHクライアント側設定
ここでは公開鍵認証方式でSSHサーバーにアクセスするクライアント側ユーザーを user01とします。
### キーペア作成
sshクライアントで使用する際に使用されるHOMEディレクトリを準備します。ここではc:\Users\user01とします。
HOME環境変数に、c:\Users\user01を設定します。
今後sshコマンドを使用する時にはこのHOME環境変数が必要になるので、ユーザーの環境変数として設定しておくことをお勧めします。
HOME以下の.sshディレクトリにキーペアを作ります。面倒なのでpassphraseは無しにします。

```
c:\Users\user01>ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/c/Users/user01/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in c:\Users\user01\.ssh/id_rsa.
Your public key has been saved in c:\Users\user01\.ssh/id_rsa.pub.
The key fingerprint is:
11:ac:89:16:e7:d1:dc:5d:96:2c:3f:8e:75:6d:89:58 user01@xxxxxxxx
The key's randomart image is:
+--[ RSA 2048]----+
|       +.. . oo. |
|    . o +.. oEo  |
|     = +.   oo. o|
|    o +  . . .+.+|
|   .    S    + + |
|            . .  |
|                 |
|                 |
|                 |
+-----------------+
```

これで、`c:\Users\user01\.ssh\`に秘密鍵(id_rsa)と公開鍵(id_rsa.pub)のファイルが作成されます。秘密鍵は盗まれるとなりすましされるので他には出さないようにアクセス権等注意しましょう。特に秘密鍵は他のユーザーからはアクセスできないようにする必要があるので、例えばファイルのパーミッションは600としておきます(644だとNG!)。

### 公開鍵の転送
上で作成した公開鍵を、SSHサーバーに転送します。転送先は、SSHサーバー側ユーザーのホーム・ディレクトリ下の.sshディレクトリです。

## SSHサーバー側設定
ここでは、公開鍵認証方式でアクセスする際に使用されるSSHサーバー側ユーザーを、sshuserとします。
### sshdの設定
sshdの構成ファイル(通常/etc/ssh/sshd_config)を編集し、公開鍵認証に関するパラメータを設定します。
具体的には以下のような設定を行います。(関連する部分のみ抜粋)

```xxx:/etc/ssh/sshd_config
Protocol 2
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeyFile   .ssh/authorized_keys
```
コメントアウトされていることがあるので、その場合はコメントアウトをはずします。設定変更したらSSHのサービスを再起動する必要があります。

### .sshディレクトリを作成
使用するユーザーのホーム・ディレクトリに.sshディレクトリを作成します。
今回の例では「/home/sshuser/.ssh」
### 公開鍵情報設定
.sshディレクトリ下にauthorized_keysファイルを作成し、入手した公開鍵の情報を追加します。

```
$ cat id_rsa.pub >> authorized_keys
```
(id_rsa.pub: クライアント側で生成した公開鍵)

### パーミッション設定
以下のようにパーミッションを設定します。
ホーム (/home/sshuser): 755
.ssh: 755
authorized_keys: 644
※自ユーザー以外の書き込み権限があるとNGなので注意！例えば、775, 664だとNGです。

## 実行例1: コマンドの実行
sshクライアント側からssh接続してみて、パスワードが聞かれなければOKです。
たとえば、Windows(sshクライアント)のコマンドプロンプトから、Linux(sshサーバー)のコマンドをssh経由で発行してみましょう。
(HOME環境変数が設定されている必要があるので注意)

```
c:\>set HOME
HOME=c:\Users\user01
c:\> ssh sshuser@hostname pwd
/home/sshuser
```

このようにパスワードを促されることなく、pwdコマンドの結果が返されればOKです。pwdの代わりに他のコマンドやシェル・スクリプトなどを指定して実行可能です。

※当該sshサーバーに最初にアクセスする際は以下のように確認のメッセージが出るのでyesと入力する必要があります。サーバー情報がHOME\known_hostsに保持されるため、次回以降は聞かれません。

```
The authenticity of host 'hostname (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is 17:4f:fa:d5:cd:9c:3f:9f:94:e3:d9:4d:9b:01:92:1a.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'hostname' (RSA) to the list of known hosts.
```

## 実行例2: シェル・スクリプトの実行
以下のテスト用シェル・スクリプトをLinux(SSHサーバー側)に用意します。

```sh:~/sh/test01.sh
#!/bin/sh

PARM1=$1
PARM2=$2
PARM3=$3
RETURNCODE=0

echo PARM1=${PARM1}, PARM2=${PARM2}, PARM3=${PARM3}

if [ ${PARM1} != 0 ]
then
        RETURNCODE=${PARM1}
fi

exit ${RETURNCODE}
```
(第一引数をリターンコードとしてそのまま使用する)

Windowsから上のシェル・スクリプトをssh経由で実行し、引数と戻り値の確認を行います。

```xxx:正常系
c:\>ssh sshuser@hostname sh/test01.sh 0 11 22
PARM1=0, PARM2=11, PARM3=22

c:\>echo %ERRORLEVEL%
0
```


```xxx:エラーケース(シェル・スクリプトのexitで0以外のリターンを意図的に返すケース)
c:\>ssh sshuser@hostname sh/test01.sh 11 22 33
PARM1=11, PARM2=22, PARM3=33

c:\>echo %ERRORLEVEL%
11
```


```xxx:エラーケース(シェル・スクリプトが意図しないエラーで異常終了)
c:\>ssh sshuser@hostname sh/test01.sh aa 11 22
PARM1=aa, PARM2=11, PARM3=22
sh/test01.sh: line 15: exit: aa: numeric argument required

c:\>echo %ERRORLEVEL%
255
```

いずれもERRORLEVELで結果を確認することが可能。
※ここでのhostnameはsshサーバー側のホスト名(or IPアドレス)を指します。

# SSH転送(ポートフォワード)
## 概要
2つのサーバー間にファイアーウォールがあり通信が制限されている場合がよくあります。Server01 => Server02へのコネクション確立はできるが逆は不可とか、特定のポートのみ通信を許可するとか。
ファイアーウォールがある環境でも、ssh(ポート22)の通信はよく使われるので、sshを含む一部の通信は許可されているということも多いです。ssh通信が可能であればssh転送(ポートフォワード)の機能で、その他の通信をsshのコネクションを使って実現することができます。トンネリングとか呼ばれるやつです。

イメージ:
![image03.gif](https://qiita-image-store.s3.amazonaws.com/0/132331/b276fef4-3ff8-c453-322d-9fc4f58cb9ae.gif)

Server01 <=> Server02間のネットワーク上はSSHでの通信しか行われていない状態で、別のTCP通信が実現できてしまいます。
ポートフォワードする通信の方向は、上の例ではServer01 <= Server02 となってますが、どちら方向でもOKです。

## teratermでの設定
Xの画面ををsshクライアントに飛ばすケースで考えてみます(WindowsからLinuxにssh接続し、Linux上でGUI画面が起動するアプリを実行し、Xの画面をWindows上に飛ばして表示させる)。
X-Windowsでは、GUIの画面を生成する側をクライアント、画面を飛ばす先(表示する側)をサーバーと呼びます。
Xのサーバー機能を提供するWindows上のフリーソフトはいくつかありますが、ここではXmingを使用することとします。

Windows上でXmingをダウンロード/インストール/起動までしておきましょう(手順は簡単なので以下の辺りを参考に)。

参考: [Xming X Server for Windows](https://osdn.jp/projects/sfnet_xming/)

teratermを開きます。最初に出てくる「新しい接続」というサブウィンドウは一旦キャンセルで閉じます。
![image04.gif](https://qiita-image-store.s3.amazonaws.com/0/132331/b95fdc9d-f46f-c4f1-c37c-72a232b8294d.gif)

設定-SSH転送を選択します。
![image05.gif](https://qiita-image-store.s3.amazonaws.com/0/132331/0443249a-d4d7-fdd0-bb07-961a6e74b4fb.gif)

追加を押します。
![image06.gif](https://qiita-image-store.s3.amazonaws.com/0/132331/a9882ba4-5a25-6dbb-5091-a99aaa735e92.gif)

リモートサーバーのポートに6000(イメージ図のPortBに相当)、ローカル側のポートに6000(イメージ図のPortAに相当)を指定します。
![image07.gif](https://qiita-image-store.s3.amazonaws.com/0/132331/f824fd9d-e598-4532-fe35-fec0c3286b97.gif)
※X-Serverはデフォルトで6000番ポートをListenするので、それをフォワードする設定をしています。

この設定をした後、teratermにてSSH接続をすると...

```
$ netstat -an | grep 6000
tcp6       0      0  ::1.6000                                      *.*                    LISTEN
tcp4       0      0  127.0.0.1.6000         *.*                    LISTEN
```

こんな感じでローカルで6000番ポートがListenされていることが分かります。
DISPLAY環境変数で飛ばし先をローカルの6000番ポートに指定してあげます。

```
$ export DISPLAY=localhost:0.0
```

これでXを使用するGUIのアプリ(xclockとかxeyesとか)を起動し、Windows側で画面が開けばOK!

※ここで例に挙げたXの画面を飛ばす例は、実はSSHのX11Forwardingの設定で簡単にできちゃいます。ただし、これはSSHサーバー側の設定なども必要になってきます。SSHサーバーの設定が自由にいじれない場合などは上の方法をご利用下さい。
参考: [X11 Forwarding設定](http://qiita.com/kou_bacchus/items/47c40beb147a3040fe5e)

# inetd使用時の注意点
SSHの構成を行う際、inetd経由で接続を行わせる構成を取る場合があります。
(最近どういう構成がはやりか分かりません。xinetdを使う、あるいはinetdとか使わずsshd単体で構成する、のかもしれません...。が、最近inetd + SSHで問題にぶち当たりましたので。)

teratermからssh接続した端末使ってシェル上での操作をする分にはあまり顕在化しないのですが、先に示したように、公開鍵認証を使ってクライアントからガシガシsshサーバーのスクリプト/コマンドを実行していると、前者の使い方に比べて単位時間あたりのssh接続要求数が跳ね上がります(後者の使い方の場合、都度ssh接続/切断となるので)。

inetdでは、特定のポートに対して意図的に高負荷をかけるようないわゆるDDoS攻撃のようなものを防ぐ手段として、単位時間当たりの接続数の上限が設定されています。
デフォルトでは、1分あたり40リクエストです。
この上限値は、inetd.confのwait/nowaitオプションに合わせて指定できます。

参考: 
[Linuxマニュアル](http://linuxjm.osdn.jp/html/netkit/man8/inetd.8.html)

>"max" ( "wait" または "nowait" からドット"."で分けられる) は、60 秒の間に最大いくつのサーバーが inetd から起動できるかを指定する。 省略された場合の、 "max" のデフォルトは 40 である。

[z/OSマニュアル](https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.1.0/com.ibm.zos.v2r1.bpxa500/inetd.htm)
>wait_flag [.max]
>
>Wait or nowait. Wait indicates the daemon is single-threaded and another request will not be serviced until the first one completes.
If nowait is specified, the inet daemon issues an accept when a connect request is received on a stream socket. If wait is specified, the inet daemon does not issue the accept. It is the responsibility of the server to issue the accept if this is a stream socket.
max is the maximum number of users allowed to request service in a 60 second interval. Default is 40. If exceeded, the service's port is shut down.

z/OS USS上のinetd.confの設定例

```
ssh       stream tcp nowait.100 SSHD  /usr/local/sbin/sshd sshd -f /usr/loc al/etc/ssh_config -i
```
nowaitの後の"100"がポイントです。この例では1分あたりの許容接続数を100に設定しています。
これは単純にsshの並行度を制御するものではないことに注意して下さい。あくまで1分あたりに**接続要求**をどれだけ受け入れるかです。間をあけて複数ssh接続を行い、それぞれのセッションを切断せずに使い続ける状況の場合、sshのセッションとしては同時に100以上保持することは可能です。

# タイムアウト
sshの機能として長時間保持されているsshのセッションを切断するというようなタイムアウト機能は無さそうです。ただし、sshクライアント-サーバー間のルーターやファイアーウォールの設定で、一定期間通信が発生しないコネクションは破棄するといったものはよく行われていると思います。
sshでは通信相手の生死を確認するkeepaliveのパケットを一定間隔で投げる機能が提供されているので、これを利用することで実際の通信が行われていない場合でもsshセッション断を回避することができます。keepaliveはsshサーバー側から行う方法、sshクライアント側から行う方法が提供されていますので、どちらか設定しやすい方を使えばよいです。

## sshサーバー側から行う方法

```xxx:/etc/ssh/sshd_conifg
ClientAliveInterval 30
ClientAliveCountMax 3
```

ClientAliveIntervalは接続しているsshクライアントに対してkeepaliveのパケットを送信する間隔を秒単位で指定します。ClientAliveCountMaxはkeepaliveの応答が無い場合にリトライする回数です。
デフォルトはClientAliveIntervalが0なので、keepaliveは発行されません。

## sshクライアント側から行う方法
クライアント側も類似のパラメーターが用意されています。

```xxx:~/.ssh/config
ServerAliveInterval 30
ServerAliveCountMax 3
```

ServerAliveIntervalは接続しているsshサーバーに対してkeepaliveのパケットを送信する間隔を秒単位で指定します。ServerAliveCountMaxはkeepaliveの応答が無い場合にリトライする回数です。
デフォルトはServerAliveIntervalが0なので、keepaliveは発行されません。

# 参考
[マルチ・プラットフォームを扱うインフラ屋さんのための連携技術: SSH編 その2](http://qiita.com/tomotagwork/items/2863bebca68d0f26a94e)
[インフラエンジニアじゃなくても押さえておきたいSSHの基礎知識](http://qiita.com/tag1216/items/5d06bad7468f731f590e)
[sshでConnection reset by peerと接続が切れるのを防ぐ](http://rcmdnk.github.io/blog/2014/08/23/computer-linux-putty/)