多段sshのウザすぎる設定を一括自動でやりたい人の遊び

# はじめに

こんにちは。最近環境構築しかやっていない人です。

サーバーに入って作業する際にsshコマンドで入って作業するのが当たり前ですが、いちいちパスワード打つのが面倒だと思います。なので公開鍵認証使ったり、踏み台サーバーを経由して多段sshしたりと色々設定する必要があります。自分の場合、作業サーバーがいっぱいなので、設定が面倒になったりしました。ウザすぎてサーバーを叩き壊したくなりました。

面倒に感じたら自動化したくなる、それが自分。ということなので実際にやってみました。今回はα版です。久しぶりにPythonでやってみるのもいいかなと思ったんですが、最近お熱なBashでまたやりました。**Bashは飽きません。シェルスクリプト最高。**

# 実装のイメージ
いざ自動化しようにも何をどうすればいいのか、どんな方法を使おうか、ない知恵を絞って一分くらい考えました。

1. .sshディレクトリがないなら作ればいいじゃない >>> mkdirで一発
2. configファイルがないなら作ればいいじゃない >>> 全サーバー情報が載ったファイルをリダイレクトで読み込んで追記していけば？
3. 踏み台サーバーの設定はどうする？ >>> 引数にして自分で決める
4. ホスト側(サーバー)での公開鍵追加も自動化したい >>> それ専用のスクリプトを送り付けて、そこで実行すれば？

こんな感じでやってけば実現できそうだなと思いました。直感全てです。

# 主要変数
こんな感じです。

```bash:item01.sh
#!/bin/bash

# SSHDIR: .sshディレクトリのパス
SSHDIR="$HOME/.ssh"
# BASEDIR: 本スクリプトのパス
BASEDIR="$(cd $(dirname $0); pwd)"
# KEYGENIN: ssh-keygenの標準入力値を格納したファイル
# 本スクリプトと同じ階層のkeygen_inputsに入っている
KEYGENIN="${BASEDIR}/keygen_inputs/$1"
# KEYPATH: 秘密鍵のパス(KEYGENINの一行目にある)
KEYPATH=`sed -n 1P "${KEYGENIN}"`
# GATEWAYNAME: 踏み台にしたいホスト名
GATEWAYNAME=$2
```
本実行スクリプトの第一引数にはssh-keygenするための入力値ファイル名を、第二引数に踏み台サーバー名を与えます。入力ファイルの中身はこんな感じです。これをssh-keygenの際にリダイレクトで叩きこみます。

なぜ改行を入れているのかというと、ssh-keygenコマンドをたたくと、まず先に秘密鍵のパスを入力します(デフォでは/home/username/.ssh/id_rsa)。そのあとパスフレーズの入力・再入力を求められます。これを入力してしまうと、サーバーログイン時にパスワードはいらないけど、パスワード的なものの入力を求められてしまいます。それを回避したいので、あえて何も入力しないように空白にしています。

```sshkeygen_inputs.in
/home/username/.ssh/key_name # 秘密鍵のパス
                             # 改行
                             # 改行
```

# .sshの作成から公開鍵の生成まで
.sshがない場合は作成・移動、あったらそのまま移動、移動したらssh-keygenで公開鍵を作る、この一連の流れをコードにしました。以下の通りです。mkdirした後にchmodで権限付与した方がいいと思ったのですが、やったらWSL上でエラーになってしまったので作るだけにしています。

コマンドが実行できなかったらエラーハンドリングして異常終了するようにしています。こうしないとスクリプトがどんどん進んでしまうので。

変数KEYGENINはファイルパスなので、ssh-keygenの際にリダイレクトしています。コメントは別に付けなくてもいいです。なんとなくです。実際にこれを実行すると、/home/username/.ssh内で秘密鍵key_nameと公開鍵key_name.pubが生成されます。

```bash:item02.sh
# ---------- .sshの作成と移動 ----------
if [ -e "${SSHDIR}" ]; then
    cd "${SSHDIR}"
else
    echo "Not existed [${SSHDIR}]. Create now."
    mkdir -p "${SSHDIR}"

    if [ $? -eq 0 ]; then
        cd "${SSHDIR}"
    else
        echo "Error: Not create [${SSHDIR}]."
        exit
    fi
fi

# ---------- 鍵の生成 ----------
ssh-keygen -C 'automatic ssh setup' <${KEYGENIN}

if [ $? -eq 0 ]; then
    echo 'Created ssh-key.'
else
    echo 'Error: Not created ssh-key.'
    exit
fi
```

# configファイルの作成
多段sshするには.ssh内のconfigファイルを編集しないといけません。一般的に以下のように設定します。インデントはしなくてもいいらしいですが、個人的にしてます。自分はHostNameをIPアドレスで指定していますが、ドメイン名でも問題ありません。

```config:config
# ----- hogeが踏み台サーバー -----
Host hoge
    User A
    HostName xxx.xxx.xxx.xxx # IPアドレスの場合
    IdentityFile /home/username/.ssh/key_name # 秘密鍵のパス

# ----- 多段ssh設定をしたいサーバー -----
Host fuga
    User B
    HostName yyy.yyy.yyy.yyy # IPアドレス
    IdentityFile /home/username/.ssh/key_name
    ProxyCommand ssh -CW %h:%p hoge # 踏み台サーバー名を指定
　　　　　　　　・
　　　　　　　　・
　　　　　　　　・
```
多段sshについて簡単に説明すると、一般的にローカルPCからあるサーバーを経由して目的のサーバーに行くためには、以下のようにsshコマンドを二回使う必要があります。これだとめんどくさいので、**どうせなら一つのコマンドでサーバーAを飛び越えてBに入りたいですし、パスワードも入力したくありません。**このショートカット方法を多段sshだと思ってます。

```
---------- 今自分のローカルPC(/home/username) ----------
$ ssh A@xxx.xxx.xxx.xxx # ssh <username>@<host-domain or host-IP>
>>> サーバーAについてのパスワード入力
---------- 今サーバーA内(/home/A) ----------
$ ssh B@yyy.yyy.yyy.yyy
>>> サーバーBについてのパスワード入力
---------- 今サーバーB内(/home/B) ----------
```

ということなので、configに設定をぶち込んでいきたいと思います。ここでは設定したいサーバー情報(ユーザー名・ホスト名・IPアドレス)を記述した入力ファイルservers.inを本スクリプトと同じ階層に配置しておき、それを参照して、configファイルに追記していきます。

```servers.in
a A xxx.xxx.xxx.xxx # ユーザー名・ホスト名・IPアドレスを空白区切りで
b B1 yyy.yyy.yyy.yyy
b B2 zzz.zzz.zzz.zzz
b B3 www.www.www.www
     ・
     ・
     ・
```
ループの前にある設定は、サーバーのタイムアウト問題を解消するために記述しています。別に各設定に組み込んでも問題ありません。ご自由にどうぞ。また変数GATEWAYNAMEで指定したホスト名(踏み台サーバー名)がループの中で現れたら、その部分の設定にはProxyCommandを付けないようにしています。それ以外に設定を施すことで、踏み台を経由した多段ssh設定ができます。servers.inを一行ずつ読み込み、setコマンドでそこからユーザー名・ホスト名・IPアドレスを取得します。

```Bash:item03_zantei.sh
echo 'ServerAliveInterval 300' > "${SSHDIR}/config"
echo 'ServerAliveCountMax 15' >> "${SSHDIR}/config"
echo 'TCPKeepAlive yes' >> "${SSHDIR}/config"
echo '' >> "${SSHDIR}/config"

chmod 755 "${SSHDIR}/config"

while read line
do
    set ${line}
    username=${1}
    servername=${2}
    ip=${3}

    echo "Host ${servername}" >> "${SSHDIR}/config"
    echo "    HostName ${ip}" >> "${SSHDIR}/config"
    echo "    User ${username}" >> "${SSHDIR}/config"
    echo "    IdentityFile ${KEYPATH}" >> "${SSHDIR}/config"

    if [ ${servername} != "${GATEWAYNAME}" ]; then
        echo "    ProxyCommand ssh -CW %h:%p ${GATEWAYNAME}" >> "${SSHDIR}/config"
    fi
done <"${BASEDIR}/servers.in"

```

# ホスト側での公開鍵追加
先のitem03_zantei.shでは、configファイルの作成しかやっていないので生成した公開鍵をホストに送り付けてauthorized_keysに追記しなくてはいけません。そこで考えたのが、**ホスト側での設定を実行するスクリプトと公開鍵を送り付けて設定する方法**です。具体的には、scpコマンドで公開鍵とそのスクリプトを送り付け、sshを介してそのスクリプトを実行するという形です。

ただお気づきだとは思いますが、それだとscpで一回、sshで一回と計二回のパスワード入力が求められます。まあ、これら一連の操作を10台のサーバーに対してやろうとするときりがないので、パスワード入力だけで済むならいいかなという感じです。妥協も大事。

ホスト側で動作するスクリプトは以下の通りです。簡単に説明すると送り付けた公開鍵を.sshディレクトリに持っていき、そこでauthorized_keysに追加・パーミッション変更するという仕様です。またこのスクリプトは仕事を終えたら自分で消えてくれます。消えなくてもいいんですけどね。

```bash:pubkey_setup_for_servers.sh
#!/bin/bash

cd $HOME
pubkey=`ls *.pub` # 公開鍵名の取得

if [ ! -e "$HOME/${pubkey}" ]; then
    echo 'ssh-public key not found.'
    exit
fi

if [ ! -e "$HOMR/.ssh" ]; then
    echo "[$HOME/.ssh] is not existed. Create .ssh"
    mkdir "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
else
    echo "[$HOME/.ssh] is existed."
fi

mv ${pubkey} $HOME/.ssh # 公開鍵を.sshに移動
cd $HOME/.ssh # そして自分も移動

# ---------- 公開鍵の追加 ----------
cat ${pubkey} >> authorized_keys
chmod 600 authorized_keys

cd $HOME

if [ $? -eq 0 ]; then
    echo 'Remove this script.'
    rm "$HOME/$0" # 用が済んだらお亡くなりになります
fi
```
このスクリプトを実行するために、item03_zantei.shにscpコマンド・sshコマンド部分を付け加えます。こんな感じです。

```Bash:item03.sh
echo 'ServerAliveInterval 300' > "${SSHDIR}/config"
echo 'ServerAliveCountMax 15' >> "${SSHDIR}/config"
echo 'TCPKeepAlive yes' >> "${SSHDIR}/config"
echo '' >> "${SSHDIR}/config"

chmod 755 "${SSHDIR}/config"

while read line
do
    set ${line}
    username=${1}
    servername=${2}
    ip=${3}

    echo "Host ${servername}" >> "${SSHDIR}/config"
    echo "    HostName ${ip}" >> "${SSHDIR}/config"
    echo "    User ${username}" >> "${SSHDIR}/config"
    echo "    IdentityFile ${KEYPATH}" >> "${SSHDIR}/config"

    if [ ${servername} != "${GATEWAYNAME}" ]; then
        echo "    ProxyCommand ssh -CW %h:%p ${GATEWAYNAME}" >> "${SSHDIR}/config"
    fi

    # ---------- 新しく追加した部分 -----------
    scp "${KEYPATH}.pub" "${BASEDIR}/pubkey_setup_for_servers.sh" "${username}@${ip}:/home/${username}"
    echo "bash /home/${username}/pubkey_setup_for_servers.sh" | ssh "${username}@${ip}"
done <"${BASEDIR}/servers.in"

```
前提として、pubkey_setup_for_servers.shも本実行スクリプトと同じ階層に配置しています。またsshを介したコマンドの実行方法は色々あるのですが、今回は、echoをパイプしてsshコマンドに渡す方法をとりました。

# 実行テスト
載せたかったんですけど、結構プライバシー的なのが移ってしまうので省略します。すんません。なので実行してみた結果と反省点をまとめてみました。

1. 純Linuxサーバー()・MacBook Proでの動作は完璧
2. WSLでは.sshディレクトリのパーミッション関連で、実行できる場合とできない場合がある(現在調査中)。
3. known_hostsをrmってから実行すると、各二回分のパスワード入力＋信頼できるホストかどうかの選択(yes/no)を迫られる。
4. configは実行するたびに書き換わるため、以前のバージョンを保持してその差分で設定をするかどうかを判断させたい(ってかそれGitじゃね？)

とまあ、こんな感じでした。

# 最後に
かなり反省点が多いですが、まだまだ作り甲斐のある代物になりそうです。今回はシェルスクリプトで実装しましたが、Pythonをラップする感じで実装してみようと思います。またかなり愚直なスクリプトになってしまったので、できるだけワンライナーに書きたいですね。頑張ってシェル芸極めます(笑)。