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アプリケーション化せずに構成できるのではないかと思われる。