2005/05/10
ここでは 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 などのように人手によるパスワートの入力を前提とするプロトコルでは適度な歯止めをかけようがない。こうした事は潜在的な問題を抱えている。
システムユーザだけにアクセスを許せば構わないポートに対しては、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 を使用していれば、POP3 before smtp の信頼性を落とすことになる。
Plan 9 の smtpd は esmtp をサポートしている。つまり、パスワード認証によってリレーの可否を決定できる。この方法であれば不正リレーを正しくカットできる。
また smtpd の -g オプションはなかなか旨く働いている。ratfs に頼らなくては筆者の環境では SPAM メールは適正レベルにまで落ちている。
/sys/lib/okip に認証に成功した IP を書き込むのは pop3 に限らなくても良いと考えられるかも知れないが、pop3 を使う利点は、
による。従って Plan9 パスワードが使われる全てのポートに IP による保護を掛ければ pop3 パスワードと Plan 9 パスワードが同時に漏れない限り侵入が困難である。
/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 でアクセスされるポートに比べてデモン型の保護は厄介である。この型には aquarela、 secstore、 fossil などが該当する。
aquarela には多数の不正アクセスが発生し、また、aquarela は 0.5 版に留まっているように十分な強度を持っていないように思える。
筆者のサーバでは secstore、 fossil への不正アクセスは未だ観測されないものの Plan 9 では重要なポートである。
これらにパッチを当てるのは避けたい。以下の方法を採用すればパッチ当ては避けられる。
デモン型のポートの保護はコネクションをリレーする事によって解決できる。
コネクションのリレーを行ってくれるプログラム relay を作成した。ソースは後に示す。
relay の仕様
relay [-l][-a okdir] from_addr to_addr
ここに
tcp!ホスト名!ポート名
の形式で与える。
の形式で与える。
方針: 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 -la /sys/log/okip tcp!hera!secstore tcp!127.0.0.1!secstore &
を実行する。
9fs は効率を落としたくはない。つまり CPU サーバから直接ファイルサーバの 9fs ポートにアクセスさせたい。従って tcp!hera!9fs はファイアーウォールの後ろに隠す事にし、tcp!ar!9fs を tcp!hera!9fs にリレーする。
CPU サーバ ar で実行する
relay -la /sys/log/okip tcp!ar!9fs tcp!hera!9fs &
実は筆者の気まぐれで 9fs のリレーは relay ではなく relay1 で行っている。relay1 の内容は
#!/bin/rc # # usage: relay1 from_addr to_addr # exec aux/listen1 $1 /bin/rc -c ' ifs=! r=`{cat $net/remote} {ip=$r(1)} if(test -e /sys/log/okip/$ip) exec connect '$2' echo Rejected
である。ここに現れる connect は con と似ているのだが、con のようによけいな事をしない。純粋に標準入出力を指定されたアドレスに転送しているだけである。con にそのようなオプションがあれば応用の可能性は広がると思うのだが今の所は無い。
aquarela はアドレスを指定できないので良い方法がない。対策としては aquarela を端末で実行し、ファイアーウォールの後ろに隠し、CPU サーバから relay で渡す事ぐらいである。aquarela はユーザ none として実行して構わないので、そうすべきだ。
もっとも筆者は Windows を使わないので aquarela もまた使わない。relay の実験は行っていないので、悪しからず。
今日はここまで...