«前の日記(2015-06-17) 最新 次の日記(2015-07-23)» 編集

meta's blog - The Power To Serve

筆者について

FreeBSDを通じてOSSにささかな貢献を。

OSS活動をご支援いただける方を募集しています


2015-06-18 Twitter でエゴサーチしたら Twitter から DoS 攻撃を食らって fork 爆弾で自爆した話

Twitter でエゴサーチしたら Twitter から DoS 攻撃を食らって fork 爆弾で自爆した話

Twitter で「エゴサーチ」(―インターネット上で、自分の本名やハンドルネーム、運営しているサイト名やブログ名で検索すること)したら、このブログが動いてるサーバの応答が遅くなり、完全に応答しなくなってしまいました。

注: ここでいうDoS攻撃とは単に特定のアクセス元から大量のトラフィックが押し寄せたことによりサーバが応答しない状態(サービス拒否状態)に陥ったことの比喩で、必ずしも悪意を持って大量のトラフィックを発生させたことを表すものではありません。

発端

事の発端は些細な出来事でした。

なんとなく当ブログについて Twitter でどう言及されているのか知ろうと思い、自分のドメイン名をキーワードに Twitter で検索を行ったのです。知っての通り、Twitter の検索結果は無限スクロールとなっておりブラウザで下端に辿り着くと、より古い検索結果が次々に表示されていく仕組みです。

そのときはただ Twitter で検索しただけで DoS 攻撃が発生するなど知る由もなく、自分のブログのドメイン名が含まれるツイートの反応を読みながら、次々にスペースバーを叩いて検索結果を下へ下へとスクロールしていました。

異常発生

数分後、サーバからアラートが上がり、当ブログのサーバに何か異常が発生していることに気づきました。

(画面キャプチャは後で再現したもので当時のものではありません)

kTs_SrQA2o7vVKBHkB9k4i-paKcir_ZA1G7fnNMrTE4=w400-no?.png NGfREYMHvwCph0Rj_cH-ca7G-FGgZHijA4y6VQAsKGY=w400-no?.png

一瞬でサーバに何が起こってるのかを理解したのも束の間、当ブログのサーバは完全に沈黙しました。

この時点で状況確認の為に接続した SSH は切断され、 再度接続を試みるも繋がらない状況になり、もはやリモートから復旧することはできず、為す術がなくなってしまいました。

復旧

帰宅後、サーバの管理コンソールにアクセスして状況を再度確認するも、一応キーボード入力に応答はするものの、プロンプトが出ず簡単に復旧できる状態ではないと判断し、強制的にリセットして再起動してしまいました。

後ほど対策を取るのですが、ここでは割愛します。

なにが起こっていたか

恐らくこういうことだと思います(この節に書かれていることは推測です)。

本来、短縮 URL を展開するだけなら例えば以下のコマンドのように t.co へ HTTP HEAD メソッドで問い合わせればよく、これによって展開された URL のサーバへのトラフィックが発生することはありません。

$ curl --silent --head http://t.co/KIt6DXkHRP | grep --ignore-case location | awk '{print $2}'

しかし t.co にアクセスしてみると、以下のような画面が表示されます。

ivZG-Hqy3KI_z0iWjXaS3BPPC-O_JEa9fl_4vnttXMg=w660-h319-no?.png

Twitter に投稿される URL をすべて t.co で短縮することにより、不利益のある活動からユーザを守ると書かれています。つまり t.co で短縮した URL が悪意のあるサイトだった場合、転送せずに警告を表示するなどの処理を行うのでしょう。

悪意のあるサイトかどうか確認するためには一度アクセスしてみなければわからないので、Twitter 内のサーバから代わりにクロールを行い、悪意のあるサイトかどうかの判定を行っていると思われます。

きっと一度クロールした結果は内部で保管していて、必ずしも毎回悪意のあるサイトかどうかの判定を行っているのではないと思いますが、検索結果をスクロールしていって URL を含むツイートが表示されるタイミングで、元の URL に トラフィックが発生するというのは観測した事実です。

なぜ応答不能に陥ったか

当ブログは見ての通り tDiary を使用しています。バージョンは 4.0.4 と最新ではないもののそこそこ新しいですが、いまどきの Ruby Web アプリケーションフレームワークのように Rack を使用した形ではなく、伝統的な CGI アプリケーションとして運用していました。

このブログは (残っている最古の記事は2007年9月ですが)2005年(10年前!)から公開しており、当時の tDiary はまだ CGI としてのみ動作するものでした。2012年4月にリリースされた tDiary 3.1.3から(?) Rack としての動作をサポートするようになりましたが、そこまでのトラフィックがなく、CGI から Rack への移行の必要性を感じていませんでした。

応答不能に陥った要因は他にも山ほどあるのですが以下にまとめます。

  1. 決して高速ではない CGI で運用している
  2. CGI で十分トラフィックを捌けていたために Rack へ移行の必要がなかった
  3. 純粋にサーバのスペックが低い (unixbenchでML115G5と同程度のスペック)
  4. Apache が CGI + prefork MPM で動いているため tDiary を実行するためのプロセスが多数 fork する
  5. サーバの性能以上のトラフィックを受け入れる設定になっていた

Twitter からのクロールにより、fork 爆弾に着火されてしまい自爆し応答不能になるという結果になりました。

対策

根本的な対応はいくらでも取れるんですが、時間とお財布の中身は有限なのでひとまず1次対応として、2つの対策を取りました。

  1. Apache の同時接続数を絞る
  2. Amazon Route 53 のヘルスチェック機能を使用しフェイルオーバーさせる

今回実行した対策以外にも robots.txt で Twitter のクローラからのアクセスを拒否またはクロール頻度を下げるという方法もあります。ただ、Twitter のクローラが robots.txt で指定したクロール頻度に従ってくれるかどうかは未確認です。たぶん従ってくれると思います。

1は fork 爆弾で自爆してハードリセットするまで再起不能にならないようにするための対策です。抜粋すると以下の様な変更です。面倒なので詳しくは説明しませんが、prefork MPM のパラメータを変更し同時に応答するリクエストを30に絞りました。

この設定では、30を超えるリクエストに対して HTTP 503 が返る訳ではなく単純にキューに入って順番に処理されます。キューの後ろのほうに入ったリクエストはただ単に待たされるだけなので、数十秒以上待たせてしまうとクライアントがタイムアウトしてしまい、ユーザから見ると以前サーバが応答しないように見えます。

本来ならキャパシティ以上のリクエストには 503 で応答し「今忙しいからちょっと待って」とクライアントに伝えなければなりませんが、とりあえずの自然回復不可能な状況を回避するための対策なのでいいんです。とりあえずだから。時間の経過でアクセス数が減少すれば再び応答可能になります。

#<IfModule mpm_prefork_module>
#    StartServers          5
#    MinSpareServers       5
#    MaxSpareServers      10
#    MaxClients          150
#    MaxRequestsPerChild   0
#</IfModule>
<IfModule mpm_prefork_module>
    StartServers          1
    MinSpareServers       1
    MaxSpareServers       3
    MaxClients           30
    MaxRequestsPerChild   0
</IfModule>

次に対策2です。1の設定変更で同時接続数は絞りましたが、同時接続数を超えるリクエストに対しては依然応答しません。アクセス数が減れば復活しますが、自然回復しなかった場合のために別の VPS に、すべての HTTP リクエストに 503 で応答する設定の Web サーバを用意します。停止時はそちらにフェイルオーバーします。以下設定の抜粋。

<VirtualHost *:80>
        ServerName      w.vmeta.jp

        RewriteEngine   on
        RewriteRule     ^.*$ - [R=503,L]

        CustomLog       /dev/null common
        ErrorLog        /dev/null
</VirtualHost>

Amazon Route 53 のヘルスチェック&フェイルオーバー具体的な設定については省略しますが、30秒毎のヘルスチェックに3回連続で応答しなかった場合、つまり90秒程度応答しない場合に DNS レベルで HTTP 503 だけを返す代わりのサーバに振り替えてくれます。

Route 53 を使用したフェイルオーバーは Amazon S3 で行う方法もありますが、別に契約してある既に稼働中の VPS を使い回し、そちらで 503 応答するようにすれば追加費用がかからなくて済むため採用しませんでした。Amazon S3 を使った場合、フェイルオーバー中も HTTP 200 でしか応答できないことも理由のひとつです。

ヘルスチェックのダウン判定までに最低90秒、ダウン検出から DNS レコードの切り替わり、TTL 切れまで考慮すると数分はかかりますが、そんなにクリティカルではないので十分です。

参考

課題

サーバが応答しなくなった場合のフェイルオーバーは良いのですが、キャパシティを超えたリクエストが単純にキューに入って待たされるだけになっているので、キャパシティを超えたリクエストに対して一旦 503 を返すようにします。

ついでに tDiary の Rack 化も進めます。

時間ができたらやります。

まとめ

Twitter の検索結果に URL が含まれる場合、検索結果の表示前後で Twitter からその URL にトラフィックが発生します。ドメイン名・URL で Twitter を検索すると、思わぬ大量のトラフィックが発生し、意図しない DoS 攻撃になる場合があります。

おねがい

良い子はまねしないでね。