スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
APCu / rate-limit · session · subnet

APCu で、守る
レート制限・失効・サブネット遮断

静的リスト(.htaccess)で止まるのは「リストに載った相手」だけ。攻撃者は名乗りを偽装し、未知のIPから来ます。 だから振る舞い=頻度で、その場で弾く動的な層が要る。APCu の TTL を軸に、レート制限・セッション失効・ サブネット遮断を組み、最後は .htaccess へ昇格させて循環を閉じます。

APCu rate limit 30/60s TTL subnet block str_starts_with

[ § 0 ] 導入

リストに載る前に、止めたい

前回.htaccess による静的遮断は、 「リストに載った相手」しか止められませんでした。ですが攻撃者は名乗り(UA)を偽装し、未知のIPから来ます。 リストに載る頃には、もう叩かれている

だから「振る舞い(頻度)」でその場で弾く、動的な層が要ります。それが APCu です。 この記事では、APCu の TTL を使ったレート制限・セッション失効・サブネット遮断の3つを実装し、 最後にそれを1本目の .htaccess昇格させて、検出から恒久遮断までの循環を閉じます。

IP はすべてマスク済み。 例に出す 203.0.113.x / 198.51.100.x / 192.0.2.x はドキュメント用予約レンジで、実在のアクセス元ではありません。
[ § 1 ] なぜ APCu か

2GB VPS の、動的防御

APCu は PHP のプロセス間で共有されるインメモリキャッシュです。DB を経由せず、超高速に読み書きできます。 リクエストごとに「今この瞬間の頻度」を数えるのに、MongoDB への往復は重すぎる。 ログの永続化は MongoDB、瞬間の判定は APCu——役割を分けます。

主役は TTL。 APCu は「N秒で自動的に消えるキー」を持てます。この自動失効が、レート制限(60秒窓のリセット)にも、 セッション(60分無操作で失効)にも、そのまま効きます。この記事のすべてが、TTL の上に乗っています。
[ § 2 ] レート制限

30req/60sec を数える

IPごとにカウンタを持ち、60秒で自動リセット。60秒間に30回を超えたら弾く。 TTL のおかげで、リセットのための後片付けは要りません。

rate-limit.php
$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 では「軽くて、十分効く」を優先。完璧より、確実に回ることを取ります。

[ § 3 ] セッション失効

uqid を APCu で持つ

iseeit の記事で「60分無操作で失効するセッション」を 分析側から語りました。その失効管理の実装が、これです。

session.php
$key = "sess:$uqid";
apcu_store($key, $now, 3600);   // アクセスのたびTTLを60分へ更新(touch)
// 60分無操作でキーが自動消滅 = セッション失効

アクセスのたびに TTL を60分へ更新(touch)するだけ。無操作が60分続けばキーが消え、セッションが失効します。 「最終アクセスから一定時間で消える」が、TTL そのもの。自前の失効判定ロジックが要りません。

揮発してよいものは、APCu に。 分析用の生ログは MongoDB に永続化し、「今生きているセッション」の状態は APCu に置く。 消えて困らないものは、TTL に任せて揮発させる——この分業が、非力なVPSを軽く保ちます。
[ § 4 ] サブネット遮断

点から、面へ

1つのIPを弾いても、同じ攻撃者が隣のIPから来ます。分散攻撃(iseeit の観点②)です。 個別IPの頻度は低く抑えられていても、サブネット単位で集計すると不自然な量が浮かびます。

subnet-guard.php
$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)から面(サブネット)へ、 対応の粒度を上げていきます。

[ § 5 ] 循環を閉じる

動的 → 静的への昇格

APCu の遮断は揮発性です(TTLで消える)。一時的な高頻度には、これで十分。 でも、何度も繰り返す悪質な常習犯は、恒久的に止めたい。そこで1本目の .htaccess へ書き戻します

escalate.php
// APCuで繰り返し捕捉したサブネットを恒久ブロックへ昇格
if ($strikes[$subnet] >= 3) {
    appendHtaccessDeny($subnet);   // 1本目の静的遮断へ書き戻し
}
検出と、2段の遮断が、1つのループになる。 iseeit(検出)→ APCu(動的・一時遮断)→ .htaccess(静的・恒久遮断)。 これは RabbitMQ の cancel 記事で「合図を出す層」と「実際に止める層」を分けたのと、同じ設計思想です。 見つける・一時的に止める・恒久的に締め出す——役割を分け、循環させます。
[ § 6 ] 体感デモ

レート制限と、昇格のはしご

複数の送信元から流れ込む60秒窓のリクエストを、APCu で集計します。閾値を動かして、 誰が通過 → 一時ブロック → 恒久(サブネット)のどの段に上がるかを見てください。 分散攻撃は個別には正常に見えても、サブネット集計で捕まります。

Interactive · Rate-limit escalation ladder
APCu 昇格ラダー
サーバー通信なし。60秒窓の集計を模擬しています。IP はマスク済み。数値は傾向を示す目安です。
レート制限の閾値(IP / 60秒)30 req / サブネット閾値 = 90
← 厳しい(誤検出↑)緩い(見逃し↑) →
通過
一時ブロック
恒久(サブネット)
閾値を選んで「60秒窓を集計する」を押してください。デフォルト30で、3つの段が出そろいます。

閾値を上げすぎると、分散攻撃も高頻度Botも見逃します。厳しくすれば正常な瞬間的アクセスまで巻き込みかねない。 その中間で、正常を通しつつ攻撃者だけを段に上げる帯があります。 そして分散攻撃は、個別のレート制限だけでは決して捕まらない——サブネット集計があって初めて面で止まります。

[ § 7 ] 総括

静的と動的は、補完しあう

APCu の TTL が、レート制限(60秒窓)・セッション失効(60分)・サブネット集計のすべてを支えていました。 静的遮断(リスト)と動的遮断(振る舞い)は補完関係で、片方だけでは守れません。 そして動的に捕まえた常習犯は、静的な .htaccess へ昇格させて恒久化する——循環が閉じます。

MongoDB / アクセスログ基盤 — 全体総括
  1. iseeit ① なぜログを MongoDB に
    器:育つデータにスキーマレス
  2. iseeit ② PVからセッションへ
    正常:uqid を束ねる
  3. iseeit ③ 攻撃者は、パターンで浮かぶ
    異常:逸脱で検出
  4. appw ① 攻撃を、止める(.htaccess)
    静的遮断:リストで恒久ブロック
  5. 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 へ昇格。検出→動的遮断→恒久遮断の循環を閉じる。

関連記事

『APCu で守る — レート制限・セッション失効・サブネット遮断』を公開しました。