暗いデータセンターに奔流のようなエメラルド色の光条が押し寄せ、上流にある発光するデフレクターにぶつかって分かれ、曲がりながら逸れていき、わずか数本のきれいな光の筋だけがその奥のマットブラックのサーバーブレードに届いている
ネットワークガイド

VPSをDDoS攻撃下でも稼働させ続ける

まったく性質の異なる三つの攻撃が「DDoS」という一つの名前を共有しており、目にするアドバイスのほとんどは、それらを一つのものとして扱うせいで的外れになります。飽和したアップリンクにnginxのディレクティブで応じたり、巧妙なレイヤー7フラッドに大きなサーバーで応じたりするのです。役に立つ問いは「どうやってこれをブロックするか」ではなく、「この攻撃は物理的にどの層で止められ、その層を誰が管理しているのか」です。このページは、レンタルしたLinuxマシンについてその問いに答えます――あなたの目に触れる前にネットワーク側が上流で吸収してくれるもの、マシンの内側で本当に効くもの、そして、たいていの場合もっとも効果的な一手が、そもそもアドレスを特定されないようにすることである理由です。

このネットワークにおけるボリューム型攻撃対策は常時稼働しており、あなたが有効化すべきものは何もありません。10Gbps未満のフラッドは静かに吸収され、10~100Gbpsはトランジットプロバイダーの側でスクラビングされ、それを超えるものは専用のスクラビング経路へと迂回されます――この閾値は、マーケティング上の形容詞としてではなくドキュメントにきちんと明記されています。それがホスト側があなたに提供できるものの、正直な限界です。そして、実際に小規模サーバーを落とす類のトラフィックにとっては、そここそが問題全体のうち、さして面白みのない半分にすぎません。

なぜなら、5ドルのVPSを確実に葬る攻撃は、ニュースになるような340Gbpsの怪物ではないからです。それは、見たこともないステートテーブルを埋め尽くす毎秒4万個の小さなSYNパケットであったり、サイト内でデータベースクエリを実行する唯一のURLへの毎秒300リクエストであったりします――完全に健全なインターフェースに、まったく飽和していないパイプを通じて届き、上流のスクラバーにはあなたの本物のユーザーとの区別がつかないトラフィックです。これらはあなた自身が対処すべきものであり、まず三種類のうちどれを見ているのかを見極めさえすれば、およそ20行の設定で対処できます。

以下はDebian 13またはUbuntu 24.04、nftables、nginxを前提としており、すでに最初の1時間の堅牢化を済ませていることも前提とします――提供していないポートにまだ応答しているサーバーは、防御を始める準備ができていません。

三つの攻撃、一つの名前

「DDoS」は意図を表す言葉であって、仕組みを表す言葉ではありません。そして、その仕組み同士にはほとんど共通点がありません。これらを正しく仕分けることは、些末な作業ではありません――それこそが作業のすべてです。なぜなら、それぞれが正確に一つの層でしか止められず、他の層からはまったく見えないからです。

ボリューム型攻撃は帯域幅を狙います。UDP増幅――DNS、NTP、memcached、そして最近では小さな質問に大きな答えを返すものなら何でも――を使えば、攻撃者は自らの1Gbpsの能力を、あなたに向けた50Gbpsへと変換できます。標的は回線であってサーバーではありません。その間、あなたのCPUはずっと暇なままです。

プロトコルおよびステート攻撃は、カーネル内の有限なテーブルを狙います。SYNフラッドはacceptキューを枯渇させようとし、汎用的な小パケットフラッドはコネクショントラッキングを枯渇させようとします。どちらもビット毎秒ではなくパケット毎秒で計測され、どちらも97%が空いている回線上でマシンを殺すことができます。これが小規模サーバーを落とすクラスであり、ほとんどのガイドが見落とすクラスでもあります。

アプリケーション層攻撃は、あなたのCPUやデータベースを狙います。使われるリクエストは本物のものと見分けがつきません。なぜならそれは実際に本物のリクエストだからです。検索エンドポイントへの毎秒100リクエストは、ネットワークにとっては何でもありませんが、PHPアプリケーションにとっては致命的です。上流のスクラバーがこれをフィルタリングすることはできません。外部から見れば、それはまさに成功しているアクセスにしか見えないからです。

この三つすべてを装って現れながら、実はまったく攻撃ではないものが四つ目として存在します。どこかでバズったリンク、あなた自身の行儀の悪いクライアント、あるいはマナーのないクローラーです。まずこれを除外することはタダでできますし、実はそれが答えだったということが恥ずかしいほどよくあります。

マシンの中からは直せない部分

あなたのアドレスに60Gbpsが向けられていて、あなたのポートが1Gbpsのポートである場合、それらのパケットをどうするかという判断は、あなたが管理するどの機器よりも数ホップ上流にあるルーターで下されます。あなたのファイアウォールがそれらを目にすることは決してありません。目にできないのです――それらは、あなたが所有していない回線を守るために、すでに破棄されているからです。これがボリューム型攻撃について構造的にもっとも重要な事実であり、「DDoS対策としてファイアウォールを堅牢化する」という助言がほとんど無意味である理由です。

したがって、意味のある問いはただ一つ、プロバイダーが自動的に何をしてくれて、その閾値がどこにあるかです。当社の閾値は、約束ではなく公表された数字です。10Gbps未満は目に見える影響なく吸収され、10~100Gbpsはトランジットプロバイダー側でスクラビングされ、わずかなレイテンシの上昇を感じることがあり、100Gbpsを超えるとプレフィックスが専用のスクラビング経路へと迂回されます――レイテンシの上昇はより顕著になりますが、サービスへの到達性は保たれます。アドレスのヌルルーティングは、より広いPoP全体を脅かすような持続的な攻撃に対する最後の手段としてのみ検討され、それが発生した場合は数分以内にお知らせします。購入すべきものも、有効化すべきものも何もありません。

スクラビングについて、ベンダーがめったに自分から語らない、知っておく価値のあることが二つあります。一つ目は、あなたが守られているのは自分自身の防御だけによってではなく、近隣によってでもあるという点です。あなたと同じ上流の/20を共有する顧客への攻撃は、誰のプレフィックスにも届く前に吸収されます――パリの隣接レンジに向けられた約340Gbpsのフラッドが、当社側では測定可能な影響なく通過したことがあり、これはまさに誰も気づかない類の結果です。二つ目は、スクラバーはヒューリスティックであり、ヒューリスティックは時に間違えるという点です。同じネットワーク上で、あるスクラバーが、隣接顧客への攻撃をフィルタリングしている最中に、正当な接続に対して9分間にわたりTCPリセットを発行し続けたことがあります。この二件のインシデントはいずれも公開インシデントログに記録されています。もし自分のセッションが、誰か他者への攻撃の最中に、タイムアウトではなく能動的なリセットのような形で切れるなら、それは実在する障害モードであり、1時間かけてローカルでデバッグするより、報告する価値があります。

何かを変更する前の、90秒間の計測

間違ったDDoS対応は、そのすべてが、何が起きているのか誰も把握する前に加えられた変更から始まります。まず四つの数字を取得してください。画面一枚に収まり、それだけで層を特定できます。

# 1. bits vs packets  — which axis is saturated?
sar -n DEV 1 5            # or: ifstat -i eth0 1

# 2. socket states     — SYN-RECV piling up means a SYN flood
ss -s
ss -tan state syn-recv | wc -l

# 3. kernel drop counters (these are the ones that matter)
nstat -az | grep -E 'ListenDrops|ListenOverflows|SyncookiesSent|TCPReqQFullDrop'
dmesg -T | tail -20       # look for: nf_conntrack: table full, dropping packet

# 4. connection tracking headroom
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

これらは組み合わせて読んでください。ビット毎秒が高く、パケット毎秒が低く、CPUも低いのはボリューム型攻撃であり、あなたの仕事はそれを確認してキーボードを打つのをやめることだけです。パケット毎秒が高く、ビット毎秒が低い――大量の小さなパケット――場合はステート攻撃であり、すぐにSyncookiesSentとconntrackのカウント数を確認してください。両軸とも大したことがないのにCPUが張り付いている場合はレイヤー7であり、答えはファイアウォールではなくWebサーバーとデータベースの中にあります。

直感に反する読み方を一つ、体に叩き込んでおく価値があります。本物のボリューム型フラッドの最中、あなたのインターフェースはほとんど穏やかに見えることがあります。あなたが見ているのは生き残りです――すでに満杯の回線を通り抜けられたもの、あるいは上流のスクラバーを通過したものです。1Gbpsのポートで900Mbpsを示しているインターフェースがあるのに、ユーザーから完全に到達不能だという報告が来るのは、攻撃ではない証拠ではありません。それこそが攻撃の姿です。

次に、そのトラフィックが単なる本物のアクセスではないことを確認してください。アクセスログをtail -fで10秒ほど眺めます。リファラーなしで数千の異なるアドレスから繰り返される一つのパスは攻撃です。普通のブラウザからの普通のパスの広がりは、それはただの訪問者であり、それにレート制限をかければ、攻撃者の仕事を代わりにやってあげるだけです。

標的は攻撃の最中ではなく、事前に小さくしておく

開いているポートはすべて、攻撃者が埋め尽くせるキューであり、ゲストが目にするパケットはすべて、たとえ最終的に破棄するとしても、CPUとconntrackのエントリをあなたに消費させます。もっとも安上がりなパケットとは、あなたのマシンがそもそも受け取らないパケットです。だからこそ、ゲストの内側でフィルタリングするより、ゲストより上でフィルタリングするほうが価値があります。

ここには二つの層があり、それらは同じものではありません。エッジフィルターは任意で、サーバーごとに設定し、ステートフルでデフォルト拒否であり、ハイパーバイザー上で動作します――そこで拒否したトラフィックは、あなたの仮想NICにはそもそも到達しないため、CPUもメモリもステートテーブルのエントリも一切消費しません。ゲストファイアウォール――あなたのnftablesルールセット――は完全にあなたのものであり、私たちが触れることは一切ありません。攻撃に接触してなお生き残るパターンは、粗くて安定したレイヤー4の真実をエッジ側で表現し(「このマシンが提供するのはこれらのアドレスからの80番、443番、SSHのみであり、それ以外は存在しない」)、アプリケーションに応じて変化するきめ細かな作業のためにゲスト側のルールセットを残しておく、というものです。両方ともファイアウォールのドキュメントに説明があります。

そのうえで、残ったものをさらに絞り込んでください。SSHを実際に管理に使うアドレスだけに制限することは、ここでは単なる堅牢化上のお作法ではありません――接続枯渇攻撃という攻撃クラスをまるごとマシンから排除することになります。127.0.0.1やWireGuardのアドレスにバインドされたデータベースは、インターネットからはそもそもフラッドできません。そして、純粋に内部向けのコントロールプレーンを提供しているなら、パスワード付きの公開ポートではなく、WireGuardインターフェースの内側に置いてください。

SYNフラッドとacceptキュー

SYNフラッドはハンドシェイクを悪用します。攻撃者は接続要求を送り続けながら決してそれを完了させず、それぞれがタイムアウトするまでカーネルのSYNキュー内の1枠を占有し続けます。キューが埋まれば、正当なハンドシェイクは破棄されます――あなたのサービスは起動していて、listenしていて、それでいて到達不能になります。

Linuxには90年代からきれいな解決策があり、デフォルトで有効になっています。SYN cookieを使うと、カーネルは半開きの接続のためにメモリを確保すること自体をやめられます。返送するシーケンス番号に接続状態をエンコードしておき、クライアントがハンドシェイクを完了させたときにそれを再構築するのです。これは思い込みではなく確認してください。そして、バーストを乗り切れるだけのゆとりをキューに持たせてください。

# /etc/sysctl.d/99-flood.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_abort_on_overflow = 0

sysctl --system

tcp_synack_retries = 2は見た目以上に重要です。デフォルトの5という値は、偽装されたハンドシェイクがおよそ3分間カーネルを占有し続けることを意味しますが、2にすればそれは約7秒まで下がります。somaxconnを引き上げるのは仕事の半分にすぎません――キューの深さはその値とアプリケーションが要求した値のうち小さいほうになるため、nginxの側にもlisten 443 ssl backlog=8192;とreloadが必要であり、それがなければカーネル側の設定には何の意味もありません。これは、適用されたように見えて実は何もしていないsysctlの典型例です。

アプリケーションに到達する前にフラッドを止めたいなら、nftablesにカーネルの代わりにハンドシェイクを完了させ、本物だと判明した接続だけを引き渡させることができます。

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    tcp dport { 80, 443 } ct state new synproxy mss 1460 wscale 7 timestamp sack-perm
    tcp dport { 80, 443 } accept
    ip protocol icmp icmp type echo-request limit rate 5/second accept
  }
}

synproxyを運用に使う前に、コンソールでまだアクセスできるマシンでテストしてください。設定を誤ると、これは自分自身を自分のサーバーから締め出す絶好の手段になります。ほとんどの人にとっては上記のsysctlブロックだけで十分であり、正直な優先順位は、まずsyncookies、次にbacklog、synproxyはこの二つで不十分だと実測できた場合に限る、というものです。

誰も見ていない、満杯になるまでは

これは、経験豊富な人ほど引っかかるものです。ステートフルなファイアウォールは、目にしたすべてのフローを覚えておく必要があり、その記憶にあたるのがnf_conntrack、つまり起動時に固定サイズが決まるハッシュテーブルです。これが満杯になると、カーネルは新規接続をすべて破棄します――攻撃者のものも、あなたの顧客のものも区別なくです――そして、誰も見ていないdmesgに一行だけログを残します。

nf_conntrack: table full, dropping packet

これがこれほど効果的である理由は、単純な算数です。小規模なVPSのデフォルトのテーブルは、せいぜい数万件程度のエントリしか保持できず、新しい送信元アドレスからのパケットはすべて1件のエントリを作ります――UDPパケットも、あなたが破棄するパケットも、フラッドそのものも例外ではありません。偽装された送信元からの毎秒2万パケットは、わずか数メガビットしか使わない回線上でも、2秒足らずでテーブルを埋め尽くします。あなたの帯域幅グラフには、何も表示されません。

# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_buckets = 65536
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
net.netfilter.nf_conntrack_udp_timeout = 20

サイズよりもタイムアウトのほうがずっと効果があります。確立済みTCPフローのデフォルトの寿命は5日間であり、これは、1週間稼働しているサーバーが、火曜日に終わった接続のステートをまだ保持し続けていることを意味します。アイドル状態のSSHセッションでもない限り1時間もあれば十分であり、net.ipv4.tcp_keepalive_time = 600を設定すれば、そうしたセッションも偶然にではなくきちんと維持されます。サイズを決める際は、1エントリあたりおよそ300バイトのカーネルメモリを見込んでください。262,144エントリで約80MBであり、これは4GBのマシンなら問題ありませんが、フォーラムがそう言っていたからという理由で1000万に設定するのは問題です。

本当にステートレスで大量のトラフィックを扱うもの――権威DNSサーバー、公開のゲームサーバー、Torリレーなど――を運用しているなら、より良い答えはそもそも追跡をやめてしまうことです。意味のある接続を持たないサービスに対するコネクショントラッキングは、純粋なコストでしかありません。

table ip raw {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    udp dport 51820 notrack
    tcp dport 9001 notrack
  }
}

そうしたトラフィックはその後ct state establishedのルールを迂回することになるため、filterチェーンに明示的なacceptルールが必要になる点を忘れないでください。それが取引の内容です。そのポートについてはステートフルネスを手放す代わりに、決して埋まらないテーブルを手に入れるのです。

レイヤー7:あなたの顧客とまったく見分けがつかないフラッド

小規模サーバーに対するもっとも効率的な攻撃は、フラッドですらありません。それは、毎秒数百件の巧妙に選ばれたHTTPリクエストであり、そのそれぞれが完全に正当であり、そのそれぞれが、インデックスのないクエリを実行するか、キャッシュされないページをレンダリングする、たった一つのエンドポイントを叩きます。この経済性は過酷です。1リクエストは攻撃者には数百バイトのコストしかかかりませんが、あなたには200ミリ秒のCPUとデータベース接続1本分のコストがかかります。あなたが提供し続けられる量がどれほどであっても、この勝負には負けます。

直感的には攻撃者をブロックしたくなります。しかし、数千の住宅用回線のアドレスに分散し、それぞれが毎秒2リクエストしか送らないボットネットに対しては、IPによるブロックは茶番です。本物のユーザーを通しつつ発動するアドレス単位のレート制限など存在せず、あなたはサイトがダウンしたままルールを追加し続けて、その障害対応の時間を使い果たすことになります。勝てる一手は、リクエストの数ではなく、コストのほうを変えることです。

まずキャッシュし、ミスもキャッシュする。ページ全体をキャッシュすれば、コストの高いリクエストがメモリ読み取りに変わります。攻撃時にもっとも重要になるディレクティブは、千件の同時ミスが千件の同時データベースクエリになってしまうのを防ぐ、このディレクティブです。

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app:64m max_size=2g inactive=60m;

server {
  location / {
    proxy_cache app;
    proxy_cache_valid 200 301 302 10m;
    proxy_cache_lock on;              # one origin request per key, not a thousand
    proxy_cache_lock_timeout 5s;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_background_update on; # serve stale, refresh behind it
    add_header X-Cache $upstream_cache_status always;
    proxy_pass http://127.0.0.1:8080;
  }
}

proxy_cache_lockuse_staleは、トラフィックの急増を生き延びられるかどうかを決める二行です。これらがなければ、ホットなキーが失効した瞬間、進行中のすべてのリクエストがオリジンへのリクエストに変わります――これが、収拾可能だったはずの攻撃を全面停止に変えてしまう殺到です。これらがあれば、バックエンドは1間隔につき1キーあたり1リクエストだけを処理し、それ以外の全員には多少古いページが返されます。これは、代替案が「ページなし」である状況では、常に正しいトレードオフです。

そのうえで、コストの高いパスだけを具体的にレート制限してください。余裕を持たせつつ、失敗したときにそれとわかる形にしておきます。

limit_req_zone $binary_remote_addr zone=general:16m rate=20r/s;
limit_req_zone $binary_remote_addr zone=costly:16m  rate=2r/s;
limit_conn_zone $binary_remote_addr zone=conn:16m;
limit_req_status 429;
limit_conn_status 429;

server {
  limit_req  zone=general burst=40 nodelay;
  limit_conn conn 24;

  location /search { limit_req zone=costly burst=5 nodelay; proxy_pass http://127.0.0.1:8080; }
  location /login  { limit_req zone=costly burst=3 nodelay; proxy_pass http://127.0.0.1:8080; }
}

nodelayを伴わないburstは、超過したリクエストを拒否するのではなくキューに入れます。攻撃を受けている最中にこれをやると、nginxがあなたの代わりに何千もの接続を律儀に開いたまま保持することになります――つまり、リクエストフラッドを接続フラッドに変換してしまったわけです。nodelayを使い、即座に429を返し、あとはクライアント側に処理させてください。そして、何らかのプロキシの背後にある場合は、set_real_ip_fromreal_ip_headerを正しく設定していない限り、$binary_remote_addrはそのプロキシのアドレスになってしまうことを忘れないでください。フロントエンド自身のアドレスをキーにしたレート制限は、それが最初に発動した瞬間、全ユーザーを一斉にロックアウトします。

Slowloris型の攻撃――大量の接続を張り、それぞれが1バイトずつリクエストを小出しに送る――はnginxのイベントモデルをほとんど困らせませんが、それでもタイムアウトは締めておく価値があります。client_header_timeout 8s; client_body_timeout 8s; send_timeout 10s; reset_timedout_connection on;limit_connと組み合わせれば、それで防御は完結します。

fail2banがDDoS対策ツールではない理由

fail2banは本当に有用なツールですが、狙っている問題が違います。ログファイルを読み、M分間にN回失敗したと判断してから、ファイアウォールルールを挿入します。そのすべての工程が、フラッドに対しては不向きな形をしています。

まず遅すぎます。40秒しか続かないバーストは、banの判定時間枠が閉じる前に終わってしまいます。ログを読むという性質上、攻撃はすでにリクエストのフルコストをあなたに払わせ終えた後です――パースが行われるのは被害が出た後なのです。個々のアドレスをbanするため、1万個の送信元に対しては、何もしないか、1万個の線形ルールを挿入するかのどちらかになり、後者の場合はファイアウォール自体がボトルネックになり、攻撃者の代わりに攻撃を完成させてしまうことになります。そして、あなたのログによって駆動される仕組みであるため、ディスクをログの行で埋め尽くすほど大きなフラッドは、あなたが想定していなかった経路からサーバーを落とすことがあります。

動的なブロッキングを行いたいのであれば、ルックアップがハッシュであり、失効が自動で行われるデータプレーンでやってください。nftablesの動的セットはサイズに関係なくO(1)であり、放っておいても自動的に忘れてくれます。

table inet filter {
  set flooders {
    type ipv4_addr
    flags dynamic, timeout
    timeout 10m
    size 65535
  }

  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    ip saddr @flooders drop
    tcp dport { 80, 443 } ct state new add @flooders { ip saddr limit rate over 50/second burst 100 packets } drop
    tcp dport { 80, 443 } accept
  }
}

このルールは注意深く読んでください。否定形がつまずきの元になるからです。アドレスがセットに追加されて破棄されるのは、レートが上限を超えているときだけです。fail2banは、それが得意とする仕事――SSHやアプリケーションのログインに対するゆっくりとした認証情報推測であり、特定のアドレスを人間の時間感覚でbanすることが正解であるようなケース――のために残しておいてください。

武器にならないこと:あなたがホストしている増幅

すべての大規模なボリューム型攻撃は、その運営者が自分が加担していることに気づいていないサーバー群によって支えられています。その仕組みは、小さく偽装されたリクエストに大きな応答を返すUDPサービスです。オープンなDNSリゾルバは、尋ねられた量の50倍を返しますし、設定を誤ったNTPやmemcachedはさらに悪質で、問い合わせてきた相手を確認する前に応答してしまうプロトコルはすべて、その候補になります。

その結果は、被害者に届くよりも先にあなた自身に降りかかります。あなたのアップリンクは自分自身の送出する応答で埋まり、プロバイダーはあなたのポートから持続的な悪用トラフィックが出ていくのを目にし、それを受け取ったトランジット業者は、その発信元レンジをヌルルーティングし始めます――だからこそ、アクセプタブルユースポリシーは、それ以外のほぼすべてを許容しつつも、攻撃の発信と増幅に対してだけは明確な一線を引いているのです。意図せずリフレクターになってしまうことは、あなたが運用しようとしていたものとは何の関係もないアドレスを失う、もっとも手っ取り早い方法です。

# bind a recursive resolver to localhost, never 0.0.0.0
# unbound: interface: 127.0.0.1  +  access-control: 0.0.0.0/0 refuse

# what is actually listening on a public address?
ss -ulpn
ss -tlpn

# from another machine, confirm you do not answer strangers
dig @your.server.ip example.com +short          # must time out
ntpq -c rv your.server.ip                        # must fail

意図的に公開UDPサービスを運用しているなら――ゲームサーバー、権威ネームサーバー、WireGuardのエンドポイントなど――ルールはどこでも同じです。レート制限すべきは応答であって、リクエストでは決してありません。bindknotはどちらも応答レート制限を実装しています。これを使ってください。それが、自分のユーザーに応答することと、誰か他人の攻撃に加担することの分かれ目になります。

もっとも効果的な防御は、見つからないこと

ここまで述べてきたことはすべて、すでにあなたのアドレスを見つけてしまった攻撃に対する被害対応です。やる価値はありますが、それは次善の策にすぎません。誰も名指しできないオリジンは、そもそもレイヤー3でもレイヤー4でも攻撃しようがなく、トラフィックを実際に処理するアドレスと、世間が知っているアドレスを分けておくことこそが、このページの中でもっともレバレッジの効く一手です。

オリジンのアドレスが漏れるのは、巧妙な攻撃を通じてであることはほとんどありません。漏れるのは、履歴と不注意を通じてであり、その原因はよく知られた短いリストに収まります。古いDNSレコードがいつもの元凶です。パッシブDNSのアーカイブは、フロントエンドを置く前にあなたが使っていたAレコードを、永遠に記憶し続けます。証明書透明性ログは公開されていて永続的であるため、オリジンに直接向いていたホスト名向けに発行された証明書は、あなたがかつてどこに住んでいたかを示す、署名付きでタイムスタンプ入りの記録になります。送信メールは、送信サーバーのアドレスをすべてのメッセージのヘッダーに刻印します。そして、静かなもう一つが、Webサーバーがベアなアドレス上でも応答してしまうことです。これがあると、アドレス空間をスキャンしてあなたのサイトのHTMLを探している誰かに、午後の時間だけでコンテンツからあなたを見つけられてしまいます。

最後のものは2行で直せますが、これをやっている人はほとんどいません。デフォルトサーバーが、あなたが提供しているホスト名を伴わずに届いたものすべてを拒否するようにしてください。

server {
  listen 80 default_server;
  listen 443 ssl default_server;
  ssl_reject_handshake on;      # nginx 1.19.4+: no certificate, no fingerprint
  return 444;                   # close without a response
}

そのうえで、フロントを選んでください。商用CDNは分かりやすい答えであり、ここで重要になる現実のコストを伴います。あなたのTLSを終端し、平文を目にし、あなたのオリジンを知り、あなたが選んだわけではない法域で法的手続きの送達を受けうる会社を、新たに追加することになるのです――これは、サーバーを名前を紐付けずに暗号資産で支払った理由の大部分を、台無しにしてしまいます。それでもやるのであれば、オリジンは一度たりとも公開されていてはならないこと、そして誰かが古いレコードを一つ見つけた瞬間、この仕組み全体が崩れることを理解しておいてください。

あなたが支払った対価としての性質を保ったままにする代替案は、自分自身がフロントになることです。別リージョンにある5ドルのインスタンスでnginxをリバースプロキシとして動かし、オリジン側のエッジフィルターをその1個のアドレスからのトラフィックだけを受け付けるようにしておけば、5分で付け替えられる捨て駒のアドレスと、あなただけに応答するコントロールプレーンが手に入ります。これはサードパーティを除いただけの、同じアーキテクチャです。そして、対象読者がそれを使える環境にあるなら、Torのオニオンサービスは、IPアドレスという要素そのものを式から完全に取り除いてくれます――フラッドすべきアドレスがそもそも存在しないのです。もっとも、オニオンサービスにもイントロダクションポイントという独自の攻撃対象領域があり、だからこそ現代のTorはまさにこれに対応するためのプルーフオブワーク(PoW)による防御を実装しています。

国、ASN、そしてトラフィックの形でブロックする

遅かれ早かれ、誰かが特定の国をブロックすることを提案してきます。これは大雑把で、たまには正しいこともありますが、大半は仕事をした気になるための手段でしかありません。

あなたのサービスが本当に限定された読者を持つ場合――地域限定のゲームサーバー、社内向けツール、管理パネルなど――であり、旅行中の自社ユーザーや、変わった経路でルーティングされている人を含む誤検知の責任を引き受ける覚悟があるなら、これは擁護できます。しかし、あなたの国も含めたあらゆる国の住宅用回線に分散している現代のボットネットに対してはほとんど役に立たず、公開サービスに対しては積極的に有害です。本物の人々を静かに失いながら、去っていった人たちを目にすることは決してないからです。

ASNでブロックするほうがまだ鋭さがあります。一握りのホスティングプロバイダー――時間単位で借りられる安価なVPSのレンジ――から発生する攻撃トラフィックは、国単位のブロックよりもはるかに少ない巻き添えで、プレフィックス単位で破棄できます。なぜなら、普通のユーザーはデータセンターからブラウジングしないからです。データセンターのレンジは、都合の良いことに、スクレイピングやクレデンシャルスタッフィングの発生源でもあります。

しかし、この発想の中で長持ちするバージョンは、地理とはまったく関係ありません。発信元ではなく、トラフィックの「形」でフィルタリングすることです。攻撃はたいてい何か構造的なものを共有しています――同じユーザーエージェント、同じ欠落したヘッダー、同じクエリパラメータを持つ同じURL、同じTLSフィンガープリント、本物のブラウザなら必ず送るはずの二つ目のリクエストの完全な不在などです。その共通の性質を見つければ、コストゼロで、攻撃者がアドレスを変えても――彼らはあなたがリストアップできるより速くそうしますが――生き残る、たった一本のルールが書けます。

# in an attack: what do these requests have in common?
awk '{print $1}'  /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # addresses
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # user agents
awk '{print $7}'  /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # paths

もし一つのユーザーエージェントがリクエストの90%を占めているなら、修正まで90秒です。もし上位20個のアドレスがそれぞれトラフィックの2%ずつを占めているなら、アドレスを探すのはやめてください――それは分散攻撃であり、答えはキャッシュとレート制限です。

アドレスが焼かれてしまったとき

攻撃が標的型で持続的であり、ランダムなアドレスではなくあなた個人を狙ったものである場合、正しい対応はそのアドレスを守ろうとするのをやめ、放棄することです。これは敗北ではありません。小規模なサービスにとっては、これがもっとも安上がりな結末であることも多く、それが痛みを伴うのは、事前にリハーサルしていなかった場合だけです。

それを迅速に行えるかどうかは、事前に決まっています。標準姿勢としてDNSのTTLを300秒に保ってください――コストはごくわずかで、その見返りとして、1日ではなく5分で移動できるようになります。デプロイを再現可能な状態に保ってください。再構築できるサーバーは、移動できるサーバーだからです。再構築するために3月に何をやったかを思い出す必要があるなら、それは計画ではなく人質です。復元可能なバックアップを別のリージョンに保管し、実際のドリルによって復元にどれだけ時間がかかるかを把握しておいてください。

そうしておけば、実際の移動作業は短くて済みます。別リージョンにデプロイし、復元し、DNSに触れる前にホスト名の上書きで新しいアドレス上での動作を検証し、レコードを切り替え、最後のリゾルバが追いつくのに十分な時間だけ旧サーバーを動かし続けてください。移行ガイドがこの順序立てを詳しく扱っており、性能のために移動するのであれ、誰かが怒っているせいで移動するのであれ、手順は同じです。

課金モデルのおかげで、このリハーサルは実質無料です。課金は残高に対して日割りで行われるため、その日の午後のうちにデプロイ・テスト・破棄する2台目のマシンは、わずか数セントで済みます。契約もカードもキャンセルすべき更新もありません。最初のアドレス付け替えを攻撃の最中に経験しなければならない理由は、どこにもありません。

これでも買えないもの

正直な締めくくりを二つ述べておきます。このページのそれ以外の部分は、構成上どうしても楽観的になっているからです。

一つ目は、あらゆるものを吸収できるホストなど存在せず、そうではないと主張するプロバイダーはただ売り込んでいるだけだということです。キャパシティは有限であり、スクラビングのキャパシティはさらに有限であり、十分に大きな規模になれば、数千の顧客を守るネットワークにとって経済合理的な行動とは、いずれ1個のアドレスのアナウンスを止めることになります。当社はこれを、より広いPoP全体を脅かす持続的な攻撃に対する最後の手段として扱っており、あなたに自力でデバッグさせるのではなく、こちらから通知します――しかし、それが絶対に起こらないという約束は嘘になってしまいますし、そう約束するどんなホストも割り引いて考えるべきです。

二つ目は、報復はメニューにないということです。ブーターやストレッサーと呼ばれるサービスは、テストツールを装った攻撃代行業者であり、関係する法域のほとんどで違法であり、広範囲に監視されており、それ以外のほぼすべてを許容する当社のポリシーの下でも、あなたのサーバーを終了させる原因になります。DDoSを攻撃者にとって魅力的にしている非対称性は、そのまま防御側にとってのそれを無価値にします。失うものが何もない相手に、フラッド量で勝つことはできません。吸収し、キャッシュし、移動し、それを退屈なものにしてしまってください――自分が送るトラフィックの代金を払っている者にとって、それは結局のところ退屈なものになるのです。

  1. 90秒で層を特定する

    設定を変更する前に、二つの軸と二つのカウンターを取得してください。ビットが高くパケットが低ければボリューム型、パケットが高くビットが低ければステート攻撃、両方とも大したことがないのにCPUが張り付いていればレイヤー7です。

    sar -n DEV 1 5                       # bits/s and packets/s per interface
    ss -s                                # socket summary; watch synrecv
    nstat -az | grep -E 'ListenDrops|ListenOverflows|SyncookiesSent'
    cat /proc/sys/net/netfilter/nf_conntrack_count
    dmesg -T | grep -i conntrack | tail

    そのうえで、成功しているだけの可能性と自傷行為の可能性を除外してください。アクセスログを10秒ほど眺めます。パスの並びが普通にサイトが使われているように見えるなら、それは攻撃者ではなく、訪問者かバグです。

  2. 攻撃からカーネルを安く守る

    ファイル2つ、reload 1回です。これらはどんな汎用サーバーでも安全に適用でき、小規模なマシンが倒れる、もっともよくある二つの経路を取り除いてくれます。

    cat > /etc/sysctl.d/99-flood.conf <<'EOF'
    net.ipv4.tcp_syncookies = 1
    net.ipv4.tcp_max_syn_backlog = 8192
    net.core.somaxconn = 8192
    net.ipv4.tcp_synack_retries = 2
    net.ipv4.tcp_keepalive_time = 600
    net.netfilter.nf_conntrack_max = 262144
    net.netfilter.nf_conntrack_buckets = 65536
    net.netfilter.nf_conntrack_tcp_timeout_established = 3600
    net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
    EOF
    
    sysctl --system
    sysctl net.ipv4.tcp_syncookies net.netfilter.nf_conntrack_max   # verify, do not assume

    そのうえで、アプリケーション側も合わせてください。nginxにはlisten 443 ssl backlog=8192;が必要です。これがなければ、あなたが今しがた広げたカーネル側のキューも、プロセスが要求した小さいほうの数値で頭打ちになってしまいます。

  3. 提供していないものはすべて、ゲストより上で閉じる

    実際に待ち受けているものを洗い出し、公開アドレス上にあってしかるべきものを見極め、粗いルールをハイパーバイザーのエッジフィルターまで押し上げて、トラフィックがそもそも仮想NICに到達しないようにしてください。

    ss -tlpn; ss -ulpn        # every public listener, with the process that owns it

    Server → Network → Firewallで、エッジフィルターをデフォルト拒否に設定し、提供しているポートだけを許可してください。SSHは管理に使うアドレスに制限します。ゲスト側のnftablesルールセットは第二の層としてそのまま維持してください――どちらか一方ではなく、二重の備えです――そして、データベースや管理インターフェースは、公開ポートをファイアウォールで守るのではなく、127.0.0.1やWireGuardのアドレスにバインドしてください。

  4. コストの高いパスの前にキャッシュを置く

    ページをレンダリングする何かにとって、これは単独でもっとも価値の高い変更です。proxy_cache_lock onproxy_cache_use_staleを伴うproxy_cacheを追加してください。そうすれば、千件の同時ミスが1件のオリジンリクエストになり、それ以外の全員には多少古いページが返るようになります。

    nginx -t && systemctl reload nginx
    curl -sI https://example.com/ | grep -i x-cache     # MISS, then HIT

    信じ込む前に、ヒット率を確認してください。すべてのレスポンスにSet-Cookieが付いているせいで一度もヒットしないキャッシュは、この一連の作業全体の中でもっともよくある「安心したつもり」のパターンです――curl -sIを2回実行し、X-Cacheを確認してください。

  5. 訪問者にではなく、あなたにコストがかかるパスをレート制限する

    全体には余裕を持たせたレート制限を、データベースに触れるエンドポイントには厳しいレート制限をかけてください。常にnodelayを使い、常にステータスコードを返し、静かにキューへ積むことは決してしないでください。

    limit_req_zone $binary_remote_addr zone=general:16m rate=20r/s;
    limit_req_zone $binary_remote_addr zone=costly:16m  rate=2r/s;
    limit_req_status 429;

    nginxが何らかのプロキシの背後にあるなら、まずset_real_ip_fromreal_ip_headerを設定してください。フロントエンドのアドレスをキーにした制限は、攻撃者だけを絞るのではありません――それが最初に発動した瞬間、全員を同時に絞ってしまいます。

  6. オリジンのアドレスを公開の目から隠す

    ベアなIPを役立たずにしてから、その前面に何を置くかを決めてください。この拒否設定はたった2行で、インターネット全体をスキャンしてあなたのサイトのHTMLを探すという経路を、恒久的に塞いでくれます。

    server {
      listen 80 default_server;
      listen 443 ssl default_server;
      ssl_reject_handshake on;
      return 444;
    }

    そのうえで、よくある順番で漏洩箇所を監査してください。パッシブDNSアーカイブに残る過去のAレコード、かつてオリジンを指していたホスト名が載る証明書透明性ログ、そして送信メールのヘッダーです。これらのどれか一つでもまだそのアドレスを示しているなら、フロントエンドを置いても救われません――先にアドレスを付け替え、フロントを置くのはその後です。

  7. 数字を添えて、ネットワーク運営者に伝える

    ボリューム型攻撃対策は自動であり、チケットを起票する必要はありません。しかし、証拠を含む報告があれば、人間の目で自動化が実際に何をしたのかを確認でき、それ自身では見えない障害モード――たとえば、スクラバーが正当なセッションをリセットしてしまうようなケース――を捕捉できます。

    宛先アドレス、開始時刻(UTC)、プロトコルと宛先ポート、あなたが計測した両軸のレート、そして自分のカウンターがパケットの到着を示しているのか、それとも上流で消えていることを示しているのかを送ってください。説明の一段落よりも、tcpdump -ni eth0 -c 200の50行のほうが価値があります。「サイトが遅いです」では対応できません。「185.x.x.x、14:02 UTC、443へのUDP、約1.2Mpps、インターフェースは40Kppsの到着を示している」であれば対応できます。

  8. 何も問題がないうちに、アドレスの付け替えをリハーサルする

    今日のうちにDNSのTTLを300秒に設定し、そのままにしておいてください。そのうえで、1時間後には破棄してしまうマシンの上で、一度だけ、最初から最後まで通しでドリルを行ってください。

    # deploy a second instance in another region, restore, verify by hostname override
    curl --resolve example.com:443:<new-ip> https://example.com/ -sI

    それに何分かかったかを書き留めておいてください。その数字こそが、このガイド内のどのルールよりも、あなたにとって本物のDDoS対策プランです。なぜなら、それこそが、標的型攻撃がどれだけの時間あなたをオフラインにし続けられるかを示す数字だからです。課金は残高に対して日割りなので、リハーサル全体の費用はわずか数セントで済みます。

比較

各攻撃を実際に止められるのはどこか

どの層も何かを止められますが、それ以外にはまったく気づきません。人々の週末を犠牲にする間違いとは、そもそもその攻撃が見えていない層で防御しようとすることです。
止められるもの止められないものあなたが払うコスト誰が管理するか
上流でのスクラビングボリューム型攻撃――10Gbps未満は吸収、100Gbpsまではスクラビング、それ以上はBGPで迂回通常のトラフィック量の範囲に収まるものすべて:ステート攻撃、レイヤー7何も。常時稼働で、チケット不要、迂回時にまれにレイテンシが生じる程度当社、自動的に
ハイパーバイザーのエッジフィルター閉じたポート宛のものはすべて、仮想NICに到達する前に――CPUもステートエントリも消費しない開けておく必要があるポートへの攻撃自分で書いたルール以外は何も。ステートフルで、デフォルト拒否あなた、サーバーごとに
ゲストファイアウォール(nftables)synproxyによるSYNフラッド対策、送信元ごとのパケットレート、望まないプロトコルすでにあなたより上流のパイプを埋め尽くしたトラフィック――そもそも届かない破棄したものも含め、パケット1個ごとにCPUとconntrackのエントリを消費あなた、全面的に
カーネルのチューニング(sysctl)acceptキューとconntrackの枯渇――小規模サーバーを葬る典型的な原因正当で完了済みの接続であるものすべてconntrackを妥当なサイズにした場合で約80MBのRAM。ファイル2つあなた、全面的に
アプリケーション(キャッシュ+レート制限)レイヤー7攻撃、リクエストの殺到、コストの高いエンドポイントパケットレベルの攻撃――そもそもWebサーバーに到達しない多少古いページ、そして見誤ったユーザーへの429エラーあなた、全面的に
リバースプロキシのフロントエンドオリジンへの直接攻撃――狙うべき公開アドレスがそもそも存在しないフロント自体への攻撃、そしてオリジンIPが漏れてしまった後のあらゆるもの月5ドルと、パッチを当て続けるべきマシンがもう1台あなた、自分で運用する場合は
商用CDN大規模なレイヤー3/4・レイヤー7攻撃を、規模を活かして、サポート窓口付きで一度でも公開されたことのあるオリジン、あるいはベアIPで応答してしまうオリジンあなたが誰かを知っていて、法的手続きの送達も受けうる第三者によるTLS終端彼ら
TorのオニオンサービスIPベースの攻撃すべて――プロトコル自体にアドレスが存在しないイントロダクションポイントへのフラッド――だからこそ現代のTorはプルーフオブワークによる防御を実装しているレイテンシ、そしてTorを進んで使ってくれる読者層あなたとネットワークの両方
FAQ

よくある質問

サーバー側でDDoS対策を有効にする必要はありますか。

いいえ。ボリューム型攻撃対策はPoPより上流に位置しており、常時稼働しています――購入すべき製品も、切り替えるスイッチもありません。10Gbps未満は目に見える影響なく吸収され、10~100Gbpsはトランジットプロバイダー側でスクラビングされ、わずかなレイテンシの上昇が見られることがあり、100Gbpsを超えるとプレフィックスが専用のスクラビング経路へと迂回され、レイテンシの上昇はより顕著になりますがサービスへの到達性は保たれます。この閾値はドキュメントで公開されています。自動ではないのは、レイヤー4より上のすべてです。ステートテーブルの枯渇やアプリケーション層のフラッドは、上流から見れば普通のトラフィックにしか見えず、あなたのマシン上で対処する必要があります。

攻撃を受けたら、自分のIPをヌルルーティングされてしまいますか。

最後の手段としてのみ、それも、あなたのサーバーだけでなくより広いPoPを脅かすような持続的な攻撃に対してのみです――そして、それが起きた場合はあなた自身に発見させるのではなく、数分以内にこちらから通知します。これが絶対に起こらないと約束するホストがあれば、それはネットワークの実態ではなくマーケティングを語っているにすぎません。十分な規模になれば、数千の顧客を守るということは、いずれ1個のアドレスを手放すことを意味するからです。現実的な防御は、自分自身を魅力のない標的にしておくことです――オリジンのアドレスを非公開に保ち、リハーサル済みのアドレス付け替え計画を持ち、復元にかかる時間を把握しておいてください。

サイトが落ちているのに、帯域幅のグラフは正常に見えます。何が起きているのですか。

ほぼ間違いなくステート枯渇です。典型的なケースはnf_conntrackテーブルの満杯です。偽装された送信元からの毎秒2万個の小さなパケットは、わずか数メガビットしか使わずにデフォルトのテーブルを数秒で埋め尽くすため、グラフには何も表示されず、新規接続はすべて破棄されます。dmesg -T | grep conntrackで「table full, dropping packet」を確認し、nf_conntrack_countnf_conntrack_maxと比較してください。もう一つの可能性は、まったく逆の読み方です。あなたより上流のパイプがすでに満杯であり、あなたのインターフェースが見せているのは攻撃そのものではなく、その生き残りだという場合です。

大きなプランにすれば、小さなプランでは耐えられない攻撃にも耐えられますか。

時には耐えられますが、多くの人が期待する理由とは違います。vCPUとRAMが多いことは、レイヤー7攻撃やステート枯渇に対しては本当に役立ちます。これらの攻撃はCPU、メモリ、テーブル領域を消耗させるものだからです。ボリューム型攻撃に対しては、プランはほとんど無関係です――パケットは、その背後に何があろうと、あなたのポートより上流で破棄されますし、2.5Gbpsのポートがあっても40Gbpsからは守ってくれません。アップグレードする前に、キャッシュとconntrackのサイズ設定を直してください。そのほうが安く済みますし、たいていの場合、それこそが本当の問題だったということになります。

no-KYCのVPSの前に、CloudflareなどのCDNを置くことはできますか。

技術的には可能ですし、うまく機能します。ただし、そうする前にトレードオフを理解してください。CDNはあなたのTLSを終端して平文を目にし、あなたのオリジンアドレスを知り、メールアドレスや多くの場合支払い方法によってあなたを特定できるアカウントを保持し、あなたが選んだわけではない法域で法的手続きの送達を受けうる存在です。名前を紐付けずに購入したサーバーにとって、これはまさに、その仕組みが避けようとしていた当事者を再び持ち込むことになります。あなたの脅威モデルが露見ではなくダウンタイムであるなら、これは妥当な選択です――ただし、オリジンは一度たりとも公開されていてはならず、そうでなければ古いDNSレコード一つですべてが台無しになります。あなたの脅威モデルに「誰が自分の居場所を知っているか」が含まれるなら、代わりに2台目のインスタンスで自分自身のリバースプロキシを運用し、オリジンにはそこからのトラフィックだけを受け付けさせてください。

fail2banだけでDDoSを止めるのに十分ですか。

いいえ。しかもそれは非力なツールというより、そもそも用途違いのツールです。ログを解析した後に人間の時間感覚で反応するため、リクエストはすでにかけるはずだったコストをすべてあなたにかけ終えています。一度に1個のアドレスしかbanしないため、1万個の送信元に対しては何もできないか、無理にやらせればファイアウォールを線形スキャンに変えてしまいます。そして、攻撃者がフラッドで埋め尽くせるログに依存しています。これが得意とする仕事――SSHやログインフォームに対するゆっくりとした認証情報推測――のために使い、フラッド並みの速度で起きることに対しては、タイムアウト付きのnftables動的セットを使ってください。

国やASNをまるごとブロックすべきですか。

国単位のブロックが擁護できるのは、あなたの読者層が本当に限定されていて、旅行中のユーザーや変わった経路でルーティングされているユーザーを失うことを受け入れられる場合だけです。あなた自身の国も含め、あらゆる場所の住宅用回線に広がっている現代のボットネットに対してはほとんど役に立ちません。データセンターのASNをブロックするほうがまだ鋭く、普通の人はホスティング用のレンジからブラウジングしないため、おまけとしてスクレイピングやクレデンシャルスタッフィングも捕まえられます。より優れた直感は、トラフィックの「形」でフィルタリングすることです。共通のユーザーエージェント、欠落したヘッダー、繰り返される一つのパスなどです。このルールは、攻撃者がアドレスを変えた後も――彼らはものの数分でそうしますが――機能し続けます。

Torのオニオンサービスを運用すれば、攻撃を受けなくなりますか。

IPベースの攻撃に対してであれば、はい、免れます――プロトコル自体にフラッドを向ける先のアドレスが存在せず、これは段階的な改善というより、質的にまったく異なるセキュリティ態勢です。ただし、全般的に免れるわけではありません。オニオンサービスはイントロダクションポイントで攻撃されることがあり、だからこそ現代のTorは、クライアント側にフラッドのコストを負わせるプルーフオブワークによる防御を実装しています。レイテンシというコストもかかりますし、Torを進んで使ってくれる読者層に限定されてしまいます。多くの人は両方を運用しています――リーチを稼ぐためのクリアネット側のフロントと、クリアネット側が攻撃を受けているときにも動き続けるオニオンアドレスです。

Deploy your offshore server.

リージョンを選ぶ。プランを選ぶ。鍵を貼り付ける。支払う。次の47秒は当社が担います。