![]() |
![]() |
2005/05/14
履歴:
ここでは TCP ポートに対する効果的なアクセス制御について紹介する。ここで問題にするのは外部からのアクセス制限である。内部のユーザからのアクセス制限に関してはプロセス制御を見よ。
筆者の CPU サーバ(ar.aichi-u.ac.jp)は大学の研究室に置かれている。plan9.aichi-u.ac.jp は ar.aichi-u.ac.jp の仮想ホストであり、実は同一のマシンである。CPU サーバは、ファイルサーバと認証サーバを兼ねた hera の下でサービスを実行している。
これらのサーバには時折いたずらの telnet や ssh ボートへのアクセスがある。現在の所、これらは単なるいたずらに留まっており、現実的な脅威ではないが、対策をとることにした。
研究室のマシンは愛知大学のファイアーウォールによって守られている。愛知大学ではサーバに外部からのアクセスを許す場合にはサーバの名称とアクセスを許可するポート番号を申請することになっている。筆者は最小限のポートを申請している。
筆者が利用しているクライアントはケーブルテレビに接続されている家庭でのクライアントと、どこで使用するか分からないノートブックである。家庭でのクライアントも固定アドレスの契約ではない*。従ってクライアントの IP アドレスを固定的に考えるわけにはいかない。
* 筆者が契約しているプロバイダーでは、かってはケーブル回線と家庭を結ぶブロードバンドルータに直接グローバルアドレスが動的に割り振られていた。しかし今はローカルアドレスが割り振られている。プロバイダはグローバルアドレスの節約のためにNAT を使っている。インターネットレベルでは筆者と同一の IP を持つ多数のクライアントが存在しているはずである。
Plan 9 の標準的な方法では DoS 攻撃に対していくつかの問題を抱えている。もっともこうした問題は Plan 9 に限らない。悪意のある大量の TCP 接続に対して何の保護もないのである。サーバは接続を確立するためにクライアントからのデータの送信を待とうとする。特に telnet 、ftp、ssh などのように人手によるパスワートの入力を想定するプロトコルでは歯止めをかけようがない。こうした事は DoS 攻撃に対して潜在的な問題を抱えている。
システムユーザだけにアクセスを許せば構わないポートに対しては、IP アドレスによるアクセス制限を最初に掛ける。そうすればポートの保護がかなり厳重になり、DoS 攻撃に対する歯止めにもなる。以下に紹介するのは筆者が採用している方法である。
筆者の方法は POP3 before smtp と良く似ている。従って telnet に適用する場合には POP3 before telnet とでも呼ぶのが適切であろう。もっと一般的に言えば POP3 before connection である。
/rc/bin/service/tcp23 を例に説明する。
#!/bin/rc
ifs=! r=`{cat $3/remote} {ip=$r(1)}
if(test -e /sys/log/okip/$ip)
exec /bin/ip/telnetd $*
echo Login rejected
/rc/bin/service/tcp23 の内容
ディレクトリ /sys/log/okip にアクセスを許可する IP アドレスと同名のファイルを作成しておく。例えば 164.46.240.3 に許可する場合にはファイル 164.46.240.3 を /sys/log/okip に作成しておくのである。クライアントからの作成は POP3 によって行う。POP3 認証に成功した場合には、成功した IP アドレスを /sys/log/okip に登録する*。
telnet ポート 23 にアクセスしているクライアントの IP アドレスは
ifs=! r=`{cat $3/remote} {ip=$r(1)}
によって分かるので* $ip と同名のファイルが存在する場合にだけ
exec /bin/ip/telnetd $*
を実行する。ここでは telnet ポートを例に説明したが、他のポートに対しても同様にやって行ける。
* $3 には例えば
/net/tcp/90
のような情報が渡される。/net/tcp/ に続く 90 はコネクション番号である。従って
cat $3/remote
の内容は
cat /net/tcp/90/remote
で得られるもの、例えば
164.46.240.3!18617
と同じものである。
なお、ip=$r(1) を { } で囲ったのは、この部分をコマンドとして扱い ifs の変更をこの行に留めたかったからである。そうしないと変更された ifs が環境変数として telnetd に引き継がれる事になる。
なぜ POP3 before smtp のメカニズムをそのまま使用しなかったのか訝る読者もいると思う。
ratfs を走らせ、その上で pop3 を -r オプションを付けて起動すると pop3 は認証をパスした IP についてディレクトリ /mail/ratify の中に例えば
'164.46.240.3#32'
のようなファイルを生成する。'#32' が付いているのは、ratfs はこのディレクトリに smtp.conf の内容を反映させるからだ。例えば smtp.conf に 164.46.240/24 からの転送要求には答えてよいよと書いてあれば ratfs は
'164.46.240.0#24'
を作成すると言った具合である。ファイル名が複雑になるのは、このディレクトリがリレーの可否をも決定しているからである。プロバイダーが NAT を使用していれば、IP アドレスが共有されているので、POP3 before smtp の信頼性を落とすことになる。
Plan 9 の smtpd は esmtp をサポートしている。つまり、パスワード認証によってリレーの可否を決定できる。この方法であれば不正リレーを正しくカットできる。
また smtpd の -g オプションはなかなか旨く働いている。ratfs に頼らなくても筆者の環境では SPAM メールは適正レベルにまで落ちている。
/sys/lib/okip に認証に成功した IP を書き込むのは pop3 に限らなくても良いと考えられるかも知れないが、pop3 を使う利点は、
による。従って Plan9 パスワードが使われる全てのポートに IP による保護を掛ければ pop3 パスワードと Plan 9 パスワードが同時に漏れない限り侵入が困難である。もちろん bootes には pop3 パスワードは必要ないし、与えない方が良い。
/sys/src/cmd/upas/pop3/pop3.c の関数 dologin の末尾に次のように syslog の1行を追加する。
enableaddr();
if(readmbox(box) < 0)
exits(nil);
syslog(0, "pop3", "OK %s", peeraddr); //Kenar
return sendok("mailbox is %s", box);
}
これで pop3 は認証が成功した IP のログを
ar Apr 12 15:56:18 OK 202.250.160.166
のように /sys/log/pop3 に残す。/sys/log/pop3 は作成しておく。
POP3 のポートは次のようになっている。
#!/bin/rc
ifs=! r=`{cat $3/remote} {ip=$r(1)}
if(! nodos $ip 110){
echo '-ERR Busy'
exit
}
/$cputype/bin/upas/pop3
a=`{tail -1 /sys/log/pop3}
if(! ~ $a(6) $ip)
exit
if(test -e /sys/log/okip/$ip)
rm /sys/log/okip/$ip
touch /sys/log/okip/$ip
/rc/bin/service/tcp110 の内容
nodos のソースは後にまた解説するが、このプログラムは nodos の引数で与えた IP と同一の IP から多数の同時アクセスが発生している場合にエラー終了する。もう一つの引数(110) は nodos のログファイルにポート番号を残すために使われている。
ディレクトリ /sys/log/okip の許可ビットは
d-rwx-wx-wx /sys/log/okip
としておく。
ユーザの none によるファイル操作は一般ユーザに比べ厳しく制限されている。そのためにファイルのタイムスタンプの更新はファイルを一旦削除して生成し直す必要がある。
このディレクトリにはシステムを構成する全てのホスト(ファイルサーバ、CPU サーバ、など)の IP が登録されている必要がある。
このディレクトリは本当はシステムユーザの悪態から保護されるべきである。しかし先に述べた許可ビットでは保護されない。この問題は、pop3 を none ではない仮想ユーザとして実行すれば解決できる。
このディレクトリの有効期間が過ぎたファイルは定期的に cron によって削除されるべきである。
listen でアクセスされるポートに比べてデモン型の保護は厄介である。この型には secstore、 fossil、aquarela などが該当する。
筆者のサーバでは secstore、 fossil への不正アクセスは未だ観測されないものの Plan 9 では重要なポートである。他方 aquarela には不正アクセスが多発し、しかも aquarela は 0.5 版に留まっているように十分な強度を持っていないように思える。
これらにパッチを当てるのは避けたい。以下の方法を採用すればパッチ当ては避けられる。
デモン型のポートの保護はコネクションをリレーする事によって解決できる。
次に示すのはコネクションのリレーを行ってくれる rc スクリプトである。
#!/bin/rc
#
# usage: relay from_addr to_addr
#
# Log is put into /sys/log/relay
# Incomming call is relayed if and only if
# it is allowed by /sys/log/okip/*
#
switch($#*){
case 1
ifs='!
' r=`{cat $net/remote} l=`{cat $net/local}{i=$r(1) p=$l(2)}
# $i is remote ip
# $p is local port
logit -l relay $p $i
if(test -e /sys/log/okip/$i)
exec connect -v $1 |[2] logit -l relay
echo Rejected
case 2
exec aux/listen1 $1 /bin/relay $2
}
relay の内容
relay の引数 from_addr と to_addr は
tcp!ホスト名!ポート名
の形式で与える。
このスクリプトの中での
$net
は
/net/tcp/90
のような値をとる。この環境変数は listen1 が与えている。$net は listen での $3 に相当する。
logit はログを採るためのプログラムで、使い方は
logit -l logname [message ...]
である。ログ情報は logit の引数からでも標準入力からでも与える事ができる。この件に関しては後に解説する。
relay に現れる connect は con と似ているのだが、con のようによけいな事をしない。純粋に標準入出力を指定されたアドレスに転送しているだけである。con にそのようなオプションがあれば connect を持つ必要は無いのだが今の所は無い。
connect の使い方は
connect [-v] addr
である。-v フラグはメッセージを標準エラーに出力する。connect に関しても後に解説する。
relay のログが欲しくなるのは最初だけかもしれない。ログが不要なら
#!/bin/rc
#
# usage: relay from_addr to_addr
#
# Log is put into /sys/log/relay
# Incomming call is relayed if and only if
# it is allowed by /sys/log/okip/*
#
switch($#*){
case 1
ifs='!
' r=`{cat $net/remote} l=`{cat $net/local}{i=$r(1) p=$l(2)}
# $i is remote ip
# $p is local port
if(test -e /sys/log/okip/$i)
exec connect $1
echo Rejected
case 2
exec aux/listen1 $1 /bin/relay $2
}
ログを採らない relay の内容
と変更すればよいであろう。
方針: secstored は tcp!127.0.0.1!secstore を聞く事にし、tcp!hera!secstore を tcp!127.0.0.1!secstore にリレーする。ローカルループバックの中に隠す事によって secstore へのアクセスは必ず relay を通る事になる。
従って認証サーバ hera で
auth/secstored -s tcp!127.0.0.1!secstore relay tcp!hera!secstore tcp!127.0.0.1!secstore &
を実行する。
注意:
relay 'tcp!*!secstore' tcp!127.0.0.1!secstore &
としてはならない。なぜなら tcp!*!secstore は tcp!127.0.0.1!secstore をも受け取る。従って悲惨な結果に終わるかもしれない。
9fs は効率を落としたくはない。つまり CPU サーバから直接ファイルサーバの 9fs ポートにアクセスさせたい。考え方が2つある。
一つは hera のサービスを il プロトコルに限定し、ar で
relay tcp!ar!9fs il!hera!9fs &
を実行しておく事。CPU サーバなどは il プロトコルで hera にマウントすることになる*。
二つ目は tcp!hera!9fs はファイアーウォールの後ろに隠し、ar で
relay tcp!ar!9fs tcp!hera!9fs &
を実行しておく事。
前者の方が厳重であるが。何れにせよ、大学の外のクライアントからは
9fs ar
でマウントできる。
注釈
* CPU サーバーは plan9.ini で
bootargs=il
を指定しておいた方が良いであろう。
aquarela はアドレスを指定できないので良い方法がない。対策としては aquarela を端末で実行し、ファイアーウォールの後ろに隠し、CPU サーバから relay で渡す事ぐらいである。aquarela はユーザ none として実行して構わないので、そうすべきだ。
もっとも筆者は Windows を使わないので aquarela もまた使わない。relay の実験は行っていないので、悪しからず。
connect を /rc/bin/service/* の中のファイルの中で使いたいと考える読者も居ると思う。その場合、先に載せたスクリプト relay は参考になるが、ローカルループバックへのリレーを行う時にはリレーが循環しないよう注意が必要である。なぜならローカルループバックへのリレーは再び listen が受け取るからである。
先に述べたリレーは全て固定されたリレーであった。場合によってはリレーの先をユーザの要求によってダイナミックに変えたいと考えるかもしれない。この場合にはユーザが無統制に listen1 を実行するのは避けなくてはならない。これはいろいろな問題を引き起こす。そこで listen1 を実行しないで、ユーザの求めに応じて任意のホストの任意のポートにリレーする方法を紹介する。
「9fs のリレー」で述べた事は次のように行う事もできる。
term% mntgen term% srv -e 'rx ar connect il!hera!9fs' foo /n/foo post... term% ls /n/foo /n/foo/386 ... /n/foo/mnt /n/foo/n /n/foo/power /n/foo/rc /n/foo/sparc /n/foo/sparc64 /n/foo/sys /n/foo/tmp /n/foo/update /n/foo/usr term%
これは筆者の家庭の Plan 9 端末から CPU サーバに rx によってアクセスし、その標準入出力を /srv/foo に繋げ、それをマウントしている。srv のこの使い方は Pipe に載っているので参考にして頂きたい。
これらはここからダウンロードできる。
注意: nodos.c には実際には活用されていないコード(工事中のもの)が含まれている。