2007年07月06日
swatchなどを使って限定サービス機能を作ってみる
「セキュリティ的にちょっと心配なサービスに対して、一時的にポート開放して接続できるようにならないかな?」というリクエストを社内から受けたので、チャレンジしてみることにした。
サービスサーバは壁の中なのでプロキシかポートフォワードさせる必要がある。サービスに接続するのは1つの動的IPのみで、ISPである程度は絞れるが、それでもかなり範囲が広い。VPNを利用するほど大掛かりではない。fail2banやpop-before-smtpのやり方の応用という着想で設計を始めた。
まず、該当サービスマシンとの接続はiprelayを使ったポートフォワードを動かすことにした。redirとかiptablesとかを使ってもいいんだけど、見通しの良さでiprelayを採用。実行は簡単。
iprelay -d 外に出すポート:サービスマシン:サービスポート
次は、どのタイミングでポートを開くか。手間がかからないこと、相手のIPアドレスがわかる方法であることが条件となる。ぱっと考えつくのはアクセス制御済みのWebでぽちっとなと押させる方法だけど、ポートを開けようとするたびに操作するのは面倒だ。そこで、syslogに吐き出されるPOP3Sのアクセスログを使うことにした。NATの裏に危ないのがいっぱいいるようだと微妙だが、とりあえずそういうNATではなさそうなのでこの方法で問題はないだろう。最初は認証サーバのdovecotのフックを使うということも考えたけど、大掛かりにすぎるし、フック失敗で全滅すると悲しいし。syslog監視にはswatchを使ってみる。
# mope-swatchrc watchfor /pop3-login: Login: user=<ユーザー>, method=PLAIN, rip=/ exec "/usr/local/bin/accept-service-mope '$_'"
要はユーザーのPOP3のログイン成功を発見したらポートを開けるスクリプトに、ログの行そのまま($_)を渡して起動するというもの。swatchの起動はこんな感じで。tailはこれでいいのか微妙なのだけど、デフォルトのはどうも挙動が怪しい気がする。
swatch --daemon --pid-file=/var/run/swatch-mope.pid \ --tail-prog=/usr/bin/tail --tail-args '--follow=name --lines=1' \ --tail-file=/var/log/syslog --config-file=/usr/local/etc/mope-swatchrc
接続ポートに対してはiptablesで許可・禁止の2つのルールを作っておく。禁止のルールはもう定義しておいてかまわない。これでデフォルトでは該当ポートには誰も接続できなくなった(もうちょっとまじめにルール書いてもいいけど)。
iptables -N mopeallow iptables -N mopedeny iptables -A mopedeny -p tcp --dport 外に出すポート -j DROP iptables -A INPUT -j mopeallow iptables -A INPUT -j mopedeny
次はスクリプトaccept-service-mopeについて。要は$_に入ってきたリモートホストのIP(ripに入ってる)に対して、iptablesで一時的に穴を開けてやればよい。後で使いやすいように時間を状態ファイルに記録する。
#!/usr/bin/perl -w
# accept-service-mope $_
use strict;
my($state) = "/tmp/mope-state"; # 状態ファイル。更新が頻繁なので気持ちとしてtmpfsにしてみた
my($rip) = "";
my($ipt) = "";
my($debug) = 0;
$rip = $1 if ($ARGV[0] =~ /rip=([\d.]+)/); # IPアドレスを取得
exit if $rip eq "";
if ( ! -f $state || state_check($state, $rip) ) { # 状態ファイルがないかIPが変更された
$ipt = "iptables -F mopeallow"; # 既存のallowルールをフラッシュする(ひどいけどまぁハックということで)
($debug) ? print "$ipt\n" : system($ipt);
$ipt = "iptables -I mopeallow -s $rip -p tcp --dport 外に出すポート -j ACCEPT"; (許可)
($debug) ? print "$ipt\n" : system($ipt);
}
state_write($state, $rip);
sub state_write { # 現在のIPと時刻を書き込み
my($state, $rip) = @_;
open(F, ">$state") || die "Can't create $state:$!\n";
print F "$rip\t" . time;
close(F);
}
sub state_check { # 現在の状態を確認
my($state, $rip) = @_;
open(F, "$state") || die "Can't open $state:$!\n";
my($l) = <F>;
close(F);
my($ip, $t) = split(/\t/, $l);
return 0 if ($ip eq $rip); # IPに変更なし
return 1;
}
これでひとまず許可の準備はできたので、POP3Sのログインを待つか、loggerコマンドでダミーの行をsyslogに発信して試してみる。
iptables -L mopeallow -n -v
Chain mopeallow (1 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT tcp -- * * IPアドレス 0.0.0.0/0 tcp dpt:外に出すポート
続いて、タイムアウトの処理のスクリプト。これをcronで1分ごとくらいで適当に回してやる。
#!/usr/bin/perl -w
use strict;
my($state) = "/tmp/mope-state";
my($limit) = 6; # 分単位。POPの頻度に合わせるとよい
my($ipt) = "iptables -F mopeallow";
exit if (!-f $state);
open(F, "$state") || die "Can't open $state:$!\n";
my($l) = <F>;
close(F);
my($ip, $t) = split(/\t/, $l);
if ($t + $limit * 60 < time) {
system($ipt);
unlink($state);
}
あとはルータでポートを外に開放して出来上がり。
この状態では複数のIPに対応していないけど、今後必要になりそうなら適宜DB化するなどすれば対処はできそう。pingする側ではroot権限がいらないので、たとえばWebアプリケーションにおいても、access.logを監視するか適当にcgiでloggerを叩くとかすれば大規模なC/Sアプリケーション化せずに構成できるのではないかと思われる。
![[hatena]](http://d.hatena.ne.jp/images/b_entry_de.gif)
![[RSS]](/d/rss10.png)