リストに載る前に、止めたい
前回の .htaccess による静的遮断は、
「リストに載った相手」しか止められませんでした。ですが攻撃者は名乗り(UA)を偽装し、未知のIPから来ます。
リストに載る頃には、もう叩かれている。
だから「振る舞い(頻度)」でその場で弾く、動的な層が要ります。それが APCu です。
この記事では、APCu の TTL を使ったレート制限・セッション失効・サブネット遮断の3つを実装し、
最後にそれを1本目の .htaccess へ昇格させて、検出から恒久遮断までの循環を閉じます。
203.0.113.x / 198.51.100.x / 192.0.2.x はドキュメント用予約レンジで、実在のアクセス元ではありません。
2GB VPS の、動的防御
APCu は PHP のプロセス間で共有されるインメモリキャッシュです。DB を経由せず、超高速に読み書きできます。 リクエストごとに「今この瞬間の頻度」を数えるのに、MongoDB への往復は重すぎる。 ログの永続化は MongoDB、瞬間の判定は APCu——役割を分けます。
30req/60sec を数える
IPごとにカウンタを持ち、60秒で自動リセット。60秒間に30回を超えたら弾く。 TTL のおかげで、リセットのための後片付けは要りません。
$key = "rate:$ip"; $n = apcu_inc($key, 1, $ok, 60); // 60秒TTLでカウントアップ if ($n > 30) { // 60秒に30回超 http_response_code(429); // Too Many Requests exit; }
最初のアクセスでキーが作られ、60秒後に自動消滅——これが窓のリセットです。 iseeit の検出記事で語った「観点①:同一IPの高頻度」を、ここで実際に止めています。
厳密なスライディングウィンドウではなく、固定窓です。2GB VPS では「軽くて、十分効く」を優先。完璧より、確実に回ることを取ります。
uqid を APCu で持つ
iseeit の記事で「60分無操作で失効するセッション」を 分析側から語りました。その失効管理の実装が、これです。
$key = "sess:$uqid"; apcu_store($key, $now, 3600); // アクセスのたびTTLを60分へ更新(touch) // 60分無操作でキーが自動消滅 = セッション失効
アクセスのたびに TTL を60分へ更新(touch)するだけ。無操作が60分続けばキーが消え、セッションが失効します。 「最終アクセスから一定時間で消える」が、TTL そのもの。自前の失効判定ロジックが要りません。
点から、面へ
1つのIPを弾いても、同じ攻撃者が隣のIPから来ます。分散攻撃(iseeit の観点②)です。 個別IPの頻度は低く抑えられていても、サブネット単位で集計すると不自然な量が浮かびます。
$subnet = subnetOf($ip); // 例: 192.0.2.0/24 $agg = apcu_inc("sub:$subnet", 1, $ok, 60); if ($agg > 90) { blockSubnet($subnet); } // 範囲ごと弾く // 所属判定は前方一致で高速に function inSubnet($ip, $prefix) { return str_starts_with($ip, $prefix); // 例: "192.0.2." }
所属判定は str_starts_with の前方一致で高速に。個別のIPは「一時ブロック」、
同一サブネットが繰り返し超過したら「範囲ごと」。点(IP)から面(サブネット)へ、
対応の粒度を上げていきます。
動的 → 静的への昇格
APCu の遮断は揮発性です(TTLで消える)。一時的な高頻度には、これで十分。 でも、何度も繰り返す悪質な常習犯は、恒久的に止めたい。そこで1本目の .htaccess へ書き戻します。
// APCuで繰り返し捕捉したサブネットを恒久ブロックへ昇格 if ($strikes[$subnet] >= 3) { appendHtaccessDeny($subnet); // 1本目の静的遮断へ書き戻し }
レート制限と、昇格のはしご
複数の送信元から流れ込む60秒窓のリクエストを、APCu で集計します。閾値を動かして、 誰が通過 → 一時ブロック → 恒久(サブネット)のどの段に上がるかを見てください。 分散攻撃は個別には正常に見えても、サブネット集計で捕まります。
閾値を上げすぎると、分散攻撃も高頻度Botも見逃します。厳しくすれば正常な瞬間的アクセスまで巻き込みかねない。 その中間で、正常を通しつつ攻撃者だけを段に上げる帯があります。 そして分散攻撃は、個別のレート制限だけでは決して捕まらない——サブネット集計があって初めて面で止まります。
静的と動的は、補完しあう
APCu の TTL が、レート制限(60秒窓)・セッション失効(60分)・サブネット集計のすべてを支えていました。 静的遮断(リスト)と動的遮断(振る舞い)は補完関係で、片方だけでは守れません。 そして動的に捕まえた常習犯は、静的な .htaccess へ昇格させて恒久化する——循環が閉じます。
- iseeit ① なぜログを MongoDB に
器:育つデータにスキーマレス - iseeit ② PVからセッションへ
正常:uqid を束ねる - iseeit ③ 攻撃者は、パターンで浮かぶ
異常:逸脱で検出 - appw ① 攻撃を、止める(.htaccess)
静的遮断:リストで恒久ブロック - appw ② APCu で守る(この記事)
動的遮断:振る舞いで一時ブロック → 昇格
検出(iseeit)→ 動的遮断(APCu)→ 恒久遮断(.htaccess)。5サイト共通ログ基盤の「観測」と「防御」が、 1つのループとして完成しました。器を選び、正常を掴み、異常を見つけ、一時的に止め、常習犯を締め出す—— 非力な2GB VPS を、軽いまま守り抜くための循環です。
この記事のまとめ
- 瞬間の頻度判定は APCu、永続化は MongoDB。役割を分ける。
- レート制限:
apcu_inc+ 60秒TTL で 30req/60sec を数え、超過は 429。 - セッション失効:
apcu_storeで TTL を60分へ touch。無操作で自動消滅。 - サブネット遮断:
str_starts_withで前方一致集計。分散攻撃を面で止める。 - 常習犯は .htaccess へ昇格。検出→動的遮断→恒久遮断の循環を閉じる。