デッドマンスイッチによるiptablesの安全な変更

古本屋で「Red Hat Linux Firewalls」の本を手に入れたのでパラパラしていたら、面白いiptablesのネタを見つけました。のでちょっと紹介します。

Red Hat Linux Firewalls (redhat PRESSシリーズ)

Red Hat Linux Firewalls (redhat PRESSシリーズ)

この本は2003年発行ということで、既にいろんな記述が古くなっているのは確か。しかしiptablesファイアウォールの基本的な考え方などはそうそう変わるもんでもないので、今読んでも参考になります。

リモートホストiptables設定時の悩み

RedHatCentOSなどRH系Linuxリモートホストsshでログインして作業しているとき、iptablesのルールを試行錯誤しながら修正したいことがあります。この場合、いきなり/etc/sysconfig/iptablesファイルはいじらずに、まずはiptablesコマンドで1行ルールを色々試してみるのが普通です。

この試行錯誤の最中に、例えばもし間違えて以下のようなコマンドを打つと、sshがブチ切られて以後何も繋がらなくなってしまいます。

# iptables -I INPUT -p tcp --dport 22 -j REJECT

これは「-A INPUT」ではなく、間違って「-I INPUT」としたため、チェーンの末尾ではなく先頭にルールを追加しちゃった! という例。わざとらしいかな?

上記のようにiptablesコマンドで設定している場合は、「# service iptables restart」さえ打てれば、/etc/sysconfig/iptablesファイルを読み込んでiptablesが再起動してくれるので、間違えて打ち込んだ1行コマンドのルールは消え失せてくれます。

ということでこのような場合はコンソールさえあれば、万一通信不可となってしまっても、コンソールからiptablesをrestartすれば良いのですが……政治的な理由でコンソールが使えなかったり、あるいは使えないことは無いがひどく使いにくい(デザイン的に・政治的に・技術的に・スペース的に)、ということもよくあります。

やりたいこと

というわけで先のようなケースに備えて、「iptablesコマンドを打ってまずルールを1行追加して、それで通信不可になってしまったら自動的にiptablesがrestartする」という動きができればいいわけです。

そんな都合の良いことができるのか、と言われると実はシェルスクリプトを使うとできるのです!

シェルスクリプトによるiptablesデッドマンスイッチ

以下のようなスクリプトになります。

#!/bin/sh

echo "iptablesを実行します。実行後、プロンプトに従いyキーを入力してください。"
echo
echo "iptables記述に誤りがあり通信不可となった場合は、yキー入力のリモートエコーが返りません。"
echo "その場合は、10秒待てば自動的にリカバリされます。"
echo

# (1) 実行したいiptablesコマンド
iptables -A INPUT -p tcp --dport 22 -j REJECT

# (2) 10秒間待ってから、iptablesをrestartする
( sleep 10; service iptables restart; ) &

echo "Enter [y] Key"
read line
case $line in
  [yY]*)
    # (3) yが入力されたら、iptablesのrestartをkillしてやめさせる
    kill $!
    ;;
  *)
    ;;
esac

まず、(1)でiptablesコマンドを実行します。ここに試行錯誤したいルールを書いておきます。

(2)がこのスクリプトのキモで、10秒sleepしてからiptablesをrestartします。つまり何もしないでボーっとしていると、(1)で実行したiptablesコマンドは10秒後に無効となります。ここではコマンド全体をカッコでくくってサブシェルとして、その最後に&を付けることでバックグラウンド起動としています。

(3)では、readコマンドで読み込んだキー入力がyだった時のみ、「kill $!」を実行しています。ここで$!とはシェルの特殊変数で、最後に実行されたバックグラウンドコマンドのプロセスIDが入っています。ここではこのプロセスIDの対象となるコマンドとは、(2)のサブシェルです。ですから$!をkillすれば(2)の処理をやめさせる、すなわちiptablesのrestartを止めることができるわけです。

readとcase

(3)のcase文は、その前のreadコマンドで読み込んだ文字列を対象としています。ここでreadコマンドは、Enterキーによる改行が無いと終了しないので、もしiptablesコマンドが間違っていてリモートホストとの接続がブチ切られてしまった場合には(3)の手前のreadコマンドで処理が止まってしまいます。結果として、(2)が10秒後に実行されてiptablesがrestartされ、(1)のiptablesコマンドがリセットされる……という仕組みです。

いやはや、なかなか面白いやり方を考えるものですね。実用性で言うと微妙だけど、何かの役に立つかもしれない。

余談

ここで出てきたデッドマンスイッチ(デッドマン装置)とは、元は「運転士が突然死しても車両が暴走するのを防ぐ」という車両の安全装置です。ここではリモート接続が切れることによってデッドマン装置が発動し、iptablesリカバリする、という動作を指しています。

デッドマン装置は、身近なところにも結構あります。例えば鉄オタには常識なのですが、電車というのは基本的に無操作ではブレーキがかかるようになっています。このため、突然運転士が倒れたり車外に放り出されても、無操作となればそのうちブレーキがかかり、列車は停止します。

この他にも、最近のフォークリフトは基本的に常にブレーキがかかっており、「ブレーキペダルを踏んでブレーキを解除」することで動くのが普通です。これはデッドマンブレーキと呼ばれます。

ちなみにこの辺の設計思想は、現代の自動車(AT車)とは大違いですね。無操作ではエンストして止まるMT車に対し、無操作では延々とクリープ現象で動き続けるAT車というのは、機械工学的に根本的な欠陥品なんじゃないかなぁ(そのせいで踏切侵入事故もありましたね)。