VPSで遊ぶ -その9 仮想コンソール無しのVPSでssh-serverがコケて外からssh接続出来ない\(^o^)/オワタ鯖にならないように、任意のタイミングでtelnet鯖のポートを別鯖に飛ばす事のできるスクリプトを仕込む。その1(問題提起編)

皆様お久しぶりです。

OpenVZ系VPS(以後VPS鯖A)って、ssh-serverの設定にミスってssh-serverが起動しない完全に繋がらない状態になった場合、仮想コンソールがないのでどうしようもないですよね。外側から永遠に接続できない糞箱が出来てしまいます。

iptablesやパスワードを初期化したところで、VPS鯖Aのssh-serverがコケテたらもうお手上げ ジ・エンドにゃん。OS再インスコか、サポートに泣きつくしかありません。

AirDisplay(AjaxTerm)がある?んなもんssh鯖が落ちたら使えないし、なるべくインスコするプログラムやサービスを太らせたくない&ポートも開けたくない。速攻粛清(手動削除)の対象にしました。

私も前回のエントリー

VPSで遊ぶ -その8 ServersMan等のOpenVZ系VPSでは /etc/init/ssh.conf内の oom never は絶対に無効にしておくこと - http://d.hatena.ne.jp/dix3/20110126/1295998428

みたいにOpenVZのVPSssh-serverがコケて詰んで、OS再インスコ&バックアップからデータ戻しという時間浪費をしてしまったことがあります。(aptitude upgradeで設定ファイルを駄目ファイルに入れ替えて繋がらなくなる罠に嵌った)

そんなリカバリー手段の少ないOpenVZ系VPSを工夫して使いたい・別系統のログイン方法が欲しい。
今日はそんなお話(の前半)。使い方は後半で。

1:対象となるユーザー

a)鯖初心者( ゚∀゚)アヒャ以外:
sshとはなんぞやとか、トンネリングって?とか、秘密鍵がーとかiptablesがーとかhosts.allowがーとかその辺りの事の説明は省略。偉大なるGoogle先生に聞きましょう。きっと導いてくれることでしょう。

b)固定のグローバルIPアドレスを持つ信頼できる別鯖(以後鯖B)を持っていて、鯖Bのssh-serverに鯖Aのsshクライアントで接続できる人:
最悪な状況でも仮想シリアルコンソールが使える別事業者のVPSや、固定IPアドレスの自宅鯖を持っている人。
同じ会社のVPSでもやってやれなくはないけれど、そっちも設定ミスで繋がらなくなると完全に詰むのでおすすめしない。(aptitude upgrade時の糞設定ファイル書き換えとかで同タイミングで2台とも繋がらなくなる事が起きうる)
鯖が1台しかない場合はここでアキラメロン

c)telnetなんか使うな糞が(゚Д゚)と速攻思い浮かべられる人
普通にtelnet鯖を立ててインターネット上に公開したらいいんでないかい?って思ってしまう人はggrks

d)プロ鯖管以外
すんません、俺が怖くなって動悸息切れしてしまうのでお断りします。
ミッション・インポッシブルな仕事には使わないでください。

上記a-dを満たす者。

iptablesや、hosts.allow,hosts.denyを自らの環境向けに適切に設定出来ない人は穴が広がる可能性が増えるだけなので決して使わないでください。

2:先に結論

じゃあVPS鯖A内にtelnet鯖を立ち上げて外部から接続できるようにポートの穴を開けるか…が出来ればいいですが(実際出来てしまいますが)インターネット上に平文垂れ流しのtelnetサーバーは流石に設置できない。

そこでssh の逆向きのポートフォワーディング(ssh -R)を使い、
緊急時に限ってVPS鯖A側のtelnet鯖を127.0.0.1縛り(localhost限定)で立ち上げて、鯖Aのtelnetのポート23をsshトンネリングで、別鯖Bに転送して使ってしまおうと。

主旨を簡潔に書くと
糞箱鯖Aのcrontabで

ssh -f -N -R 鯖Bの適当な空きポート番号:localhost:23 鯖Bユーザ@鯖BのIPアドレス 

を何かのきっかけ(トリガー)で起動し、
鯖B側で

telnet localhost 鯖Bの適当な空きポート番号

すると鯖A接続キタ━━━(゚∀゚)━( ゚∀)━(  ゚)━(  )━(  )━(゚  )━(∀゚ )━(゚∀゚)━━━!!! 急いでssh-serverが起動するように修復するんだ!

…てな具合でssh-serverのコケた鯖Aの内側から、外側鯖Bのssh-serverにssh(クライアント)でトンネルを掘って、糞箱鯖Aの内側にアクセスしてしまおうというわけです。

telnet over ssh を外部から任意のタイミングで実行出来るようにします。それ以上でもそれ以下でもないです。



環境

必要となる環境はこんな感じ。後半のエントリーでもう少し丁寧に手順を書きます。

  • 鯖A,鯖B共に Ubuntu 10.04.2 LTS i686、他の環境?シラネ
  • telnetd autossh を鯖Aに追加インストール(autosshじゃなくてもいいが、コネクション切れ時に自動再接続させたいので)、鯖Aのtelnetdには、鯖A内からのみ接続できるようにiptables,hosts.allow,hosts.deny,xinetdを調整しておく。
  • 実行ユーザー(ここではhogeusr)が鯖A→鯖B(のここではsfconsoleユーザー)にsshでノーパスワードで接続可能。(ssh-keygen時にノーパスワードでつながるようにしておく。無理ならexpectでも使って自動入力頑張ってください。)
  • 鯖Aのcrontabが動いている、sshクライアントも動いている。(ssh-serverやsshを削除しちゃったらこれでも復旧できない)


3:トリガーチェック&自動トンネル掘りスクリプト

説明は次のエントリーで。

このスクリプト(鯖Aで実行)では
鯖B側作業ディレクトリ(例: sfconsole@59.xxx.xxx.xxx:/home/sfconsole/exec_autossh/)に
鯖A(183.xxx.xxx.xxx)のipアドレスの空ファイル (例:sfconsole@59.xxx.xxx.xxx:/home/sfconsole/exec_autossh/183.xxx.xxx.xxx)
が存在する場合にはtelnet over sshの 自動穴掘り指令が下った緊急事態とみなし、

ssh -f -N -R 鯖Bの適当な空きポート番号:localhost:23 鯖Bユーザ@鯖BのIPアドレス 

を実行します。(+多重起動防止チェック、pidファイル除去、鯖Bの作業ディレクトリ側へのログ書き込み付き。& autossh使用)

あとはこいつをcrontabで鯖A側で1時間に1回くらい回せばおk、ssh-serverが起動しなくなっても、
鯖Bで空ファイルをtouchすると、遅くても1時間後にはトンネル掘ったtelnetで、鯖B側から鯖Aにログインしてリカバリー作業が出来る様になります。




ソース1:鯖A(OpenVZ-VPS鯖)側設置用、閉じられた内側から自動トンネル掘りを試すスクリプト

/home/hogeusr/bin/check_sfconsole.sh

#!/bin/sh
#鯖A側の一般ユーザ(hogeusr)のcrontab -e に仕込んで定期実行する。
# m h  dom mon dow   command
#*/60 * * * * /home/hogeusr/bin/check_sfconsole.sh > /dev/null
LANG=C
#鯖B(Xen,KVM,または実機)側IPアドレス
REMOTE_IP="59.xxx.xxx.xxx"

#鯖B側ssh接続ポート番号(通常22)
REMOTE_PORT="10122"

#鯖B側ssh接続ユーザ名(緊急作業時専用ユーザ、ノーパスワードで接続出来るようにssh-keygen -t rsa の時に空パスで接続可能にしておくこと)
REMOTE_USR="sfconsole"

#鯖A(OpenVZ)側IPアドレス
LOCAL_IP="183.xxx.xxx.xxx"

#鯖B側の作業ディレクトリ名
DIRNAME="exec_autossh"

#鯖Aのtelnet用port 23を、鯖B側の下記のポートで接続可能とする。 (鯖Bで telnet localhost 10124 )
TMP_PORT="10124"

#トンネル堀り開始指令用ファイルチェック
check_pid(){
  if ssh -p ${REMOTE_PORT} ${REMOTE_USR}@${REMOTE_IP} "ls ${DIRNAME}/${LOCAL_IP} && rm ${DIRNAME}/${LOCAL_IP}"  > /dev/null 2>&1 ;
then
    echo "pid file ( ${DIRNAME}/${LOCAL_IP} ) exists"
    return 0 ;
  else
    echo "pid file ( ${DIRNAME}/${LOCAL_IP} ) NOT exists"
    return 1 ;
  fi
}

#autossh多重起動防止チェック
check_ps(){
  if ps axu |grep [a]utossh.*${REMOTE_USR}@${REMOTE_IP}  > /dev/null 2>&1 ; then
    echo "autossh already running "
    return 1 ;
  else
    echo "autossh not running , now start up"
    return 0 ;
  fi
}

#autossh起動、トンネル掘り開始 鯖Aの23番ポートを鯖Bの${TMP_PORT}に飛ばす
exec_autossh(){
  if autossh -M 0 -p ${REMOTE_PORT} -f -N -R ${TMP_PORT}:localhost:23 ${REMOTE_USR}@${REMOTE_IP} ; then
    echo "autossh start up OK"
    return 0 ;
  else
    echo "autossh start up FAIL"
    return 1 ;
  fi
}

#ログファイルを鯖B側にssh経由で書き込み
write_log(){
  MSG="`date`: autossh -M 0 -p ${REMOTE_PORT} -f -N -R ${TMP_PORT}:localhost:23 ${REMOTE_USR}@${REMOTE_IP} START"
  if ssh -p ${REMOTE_PORT} ${REMOTE_USR}@${REMOTE_IP} "echo ${MSG} >> ${DIRNAME}/${LOCAL_IP}.log"  > /dev/null 2>&1 ; then
    echo ${MSG}
    return 0 ;
  else
    return 1 ;
  fi
}

#関数呼び出し実行 main
if check_pid && check_ps && exec_autossh ; then
write_log
fi




ソース2:鯖A(OpenVZ-VPS鯖)側設置用、ソース1の定期実行 sfclientユーザー(一般ユーザー)の crontab -e

# m h  dom mon dow   command
*/60 * * * * /home/sfclient/bin/check_sfconsole.sh > /dev/null


ソース3:鯖A(OpenVZ-VPS鯖)側設置用、/etc/hosts.allow (一例)

# /etc/hosts.allow: list of hosts that are allowed to access the system.
#                   See the manual pages hosts_access(5) and hosts_options(5).
#
# Example:    ALL: LOCAL @some_netgroup
#             ALL: .foobar.edu EXCEPT terminalserver.foobar.edu
#
# If you're going to protect the portmapper use the name "portmap" for the
# daemon name. Remember that you can only use the keyword "ALL" and IP
# addresses (NOT host or domain names) for the portmapper, as well as for
# rpc.mountd (the NFS mount daemon). See portmap(8) and rpc.mountd(8)
# for further information.
#
ALL:127.0.0.1
sshd:ALL


ソース4:鯖A(OpenVZ-VPS鯖)側設置用、/etc/hosts.deny (一例)

# /etc/hosts.deny: list of hosts that are _not_ allowed to access the system.
#                  See the manual pages hosts_access(5) and hosts_options(5).
#
# Example:    ALL: some.host.name, .some.domain
#             ALL EXCEPT in.fingerd: other.host.name, .other.domain
#
# If you're going to protect the portmapper use the name "portmap" for the
# daemon name. Remember that you can only use the keyword "ALL" and IP
# addresses (NOT host or domain names) for the portmapper, as well as for
# rpc.mountd (the NFS mount daemon). See portmap(8) and rpc.mountd(8)
# for further information.
#
# The PARANOID wildcard matches any host whose name does not match its
# address.
#
# You may wish to enable this to ensure any programs that don't
# validate looked up hostnames still leave understandable logs. In past
# versions of Debian this has been the default.
# ALL: PARANOID
ALL:ALL



ソース5:鯖A(OpenVZ-VPS鯖)側設置用、/etc/xinetd.d/telnet (一例)

# default: off
# description: Telnet server
# securlevel: 30
service telnet
{
        disable         = no
        socket_type     = stream
        protocol        = tcp
        wait            = no
        user            = root
        server          = /usr/sbin/in.telnetd
        server_args     = -h
        only_from       = localhost.localdomain
} 



今日はここまで。
インスコ方法や使い方、使用例などは後半に続く。
皆まで書かずに終わりにしたくなってきたけど続けます。

ではでは!