前回の記事では、iPhoneのSafariから「小説家になろう」を開くと403になり、iCloudプライベートリレーを経由しないよう「IPアドレスを公開」にすると閲覧できることを確認した。

症状の切り分け方や閲覧するための対処方法は、前の記事にまとめている。

その後も確認を続けると、Private Relayを有効にしていれば必ず403になるわけではなかった。

Private Relayの出口結果
Cloudflare今回確認した3IPは403
Akamai403 / 正常表示
Fastly正常表示
「IPアドレスを公開」正常表示

同じiPhone、同じSafari、同じURLでも、Private Relayで割り当てられる出口IPによって結果が変わっている。

この記事では、仮に今回の403が攻撃・自動アクセス対策の一部だとしたら、IPやネットワーク単位の制限が何に効くのかを考える。

そのうえで、一般読者まで403にする前に、配信側でオリジンへのアクセス自体を減らせないか、自分ならどう設計するかまで考えてみる。

ヒナプロジェクトの内部設定や今回の403ルールは公開されていない。特定のIPやIPレンジ、ASN、IP Reputation、Proxy判定、レート制限など、実際にどの条件で拒否されているかは外から確認できない。

ヒナプロジェクトがDDoSを現実の問題として抱えているのは事実

IDCフロンティアが2024年に公開した導入事例では、ヒナプロジェクトがIDCFクラウドとDDoS対策サービスを利用していることが紹介されている。

導入理由としても、外部からの悪意ある攻撃によってWebサイトへアクセスしにくくなることがあったと説明されている。

2025年3月18日には「小説家になろうグループのサイトを含むネットワーク」に対する大規模DDoS攻撃が発生し、4月9日にも再び大規模DDoS攻撃を受けている。

その前日の3月17日には、アクセス解析システムKASASAGIも海外からDDoS攻撃を受けたと告知されている。

そのため、今回の403を見て攻撃・自動アクセス対策を連想すること自体は不自然ではない。

ただし、「DDoS対策」という言葉だけでは範囲が広すぎる。

帯域を埋めるDDoSとHTTP Floodは分けて考える

DDoSには、回線帯域を埋める攻撃もあれば、通常のHTTPリクエストを大量に送る攻撃もある。

攻撃・負荷の位置主な例主な対策
L3/L4UDP Flood、ICMP Flood、SYN FloodなどScrubbing、Anycast、BGP FlowSpec、SYN Proxyなど
TLS / ConnectionTLS handshake大量発生、connection exhaustionEdge TLS終端、Connection Limitなど
L7HTTP Flood、API Flood、bot、scrapingWAF、Rate Limit、Bot Managementなど
Application / オリジン重いDB処理、検索、キャッシュミス大量発生Cache、Load Shedding、Concurrency Limitなど

今回観測したのはHTTP 403だ。HTTPリクエストを受けたうえでアクセス可否を判断しているため、今回のような制御がDDoS対策と関係しているとしても、L7 HTTP Floodやbot、自動アクセス対策に近い。

帯域を埋める攻撃は、オリジン側で403を返しても、その手前のネットワーク帯域を守れない。

Internet
  ↓
大量のトラフィック
  ↓
回線・上流ネットワーク
  ↓
Edge / LB / オリジン

一方、

GET /n4402bd/1/
GET /n4402bd/1/
GET /n4402bd/1/
...

のようなHTTP Floodなら、WAFやRate Limitで早い段階に落とす意味がある。

IP・ネットワーク単位の制限は何に効くのか

目的ごとに見ると、向いているものと限界が分かる。

想定する問題IP・ネットワーク制限の効果限界正規ユーザーへの影響
Volumetric DDoS主対策にはなりにくいオリジン側の403では上流の帯域飽和を防げない制限対象だけ利用不能になる
SYN / TLS系DDoS主対策ではないより前段の接続制御が必要同上
L7 HTTP Flood一定の効果がある分散されるとIP単位では弱い共有IPの読者を巻き込む
大量スクレイピング単純なbotには効くIPローテーションやResidential Proxyで回避できるPrivate Relay利用者も対象になり得る
脆弱性スキャンノイズ削減には有効攻撃元を変えれば継続できる同じネットワークの正常アクセスも失う
高度な分散bot限定的IPだけでは識別しにくい誤検知が増えやすい
広告・リワード不正補助信号にはなるIP単独では判定できない正規閲覧や広告表示まで失う可能性

ネットワーク属性を第一防衛線の一つとして使うこと自体は珍しくない。

AWS WAFにもIP Reputationルールがあり、IP ReputationやRate Limitは入口で使える対策の一つになっている。

ただし、回避する側は別のクラウド、VPN、Residential Proxy、モバイル回線などへ移れる。

IPを見ること自体が問題なのではなく、IPだけでは正規ユーザーと攻撃者を強く識別できない。

WAFを入れればPrivate Relayが403になるわけではない

一般的なWAFを導入しただけでPrivate Relayを一律に拒否するのが標準動作、というわけではない。

WAFやIP Intelligenceサービスには、IP Reputation、匿名Proxy、ホスティングネットワーク、ASN、レート制限などを判定材料として使う機能がある。

Apple自身も、Private RelayのIPがGeoIPデータベースなどで is_relay、is_hosting、privacy_service、privacy_proxy といった属性で分類される場合があると説明している。

Private Relayの出口IPリスト、WAF、Rate Limit、GeoIP、アクセス解析まで含めたWebサービス側の対応は、別の記事にまとめた。

例えば次のどれでも、今回に近い挙動は作れる。

Private RelayのIPレンジ
→ ブロック

特定ASN
→ ブロック

privacy_proxy
→ 高リスク扱い

IP Reputationが一定以下
→ ブロック

IP単位のレート上限超過
→ ブロック

今回確認したCloudflareの出口IPは3つとも403だった。一方、Akamaiでは403になるIPと正常表示できるIPの両方があった。

少なくとも「Private Relay全体を拒否」「Akamai全体を拒否」という単純なルールでは、この結果を説明できない。

Cloudflareについても、今回確認した3IPが403だったことから、CloudflareのASN全体を拒否しているとまでは言えない。

配信構成の追加調査で、403の見方も変わった

403を調べる途中で、DNSとHARから配信先も追い直した。

当初は「IDCFとさくらインターネットのどちらへ接続されるかで403が変わる」と考えていたが、追加調査では、本文HTMLの ncode.syosetu.com はIDCF側、JavaScriptやCSSなどの静的ファイルはさくらインターネット側から配信されていた。

つまり、本文HTMLの接続先がIDCF側でも、Private Relayの出口IPによって403と正常表示が分かれている。

DNS、Amazon Route 53、ASN、HAR、IDCFとさくらインターネットの役割分担、マルチクラウドの可用性については別の記事にまとめた。

この記事では、ここから配信インフラそのものではなく、IPベースのアクセス制御に頼りすぎずにオリジンをどう守るかを考える。

本文は完成HTMLに含まれている

実際の小説ページをブラウザから確認すると、本文はJavaScriptであとからAPI取得する構成ではなく、初期HTMLに含まれていた。

例として確認した作品ページはこちら。

作品情報やエピソード一覧も初期HTMLに含まれ、エピソードページでは本文そのものがHTMLとして返される。

フロントエンドから確認できるのは、本文を含む完成HTMLをサーバーから返すMPA型の構成だ。

内部でリクエストごとにHTMLを生成しているのか、一部を事前生成しているのか、ncode.syosetu.com の本文HTMLにどのような共有キャッシュがあるのかまでは外から確認できない。

追加調査では、JavaScriptやCSSなどの静的ファイルについては、さくらインターネット側でApache Traffic Serverを使ったキャッシュ配信が行われていることを確認している。

そのため、ここで考えたいのは静的ファイルではなく、本文を含むHTMLの方だ。

寿命の違う情報が同じHTMLに入っている

小説本文は、作者が編集しなければほとんど変わらない。

一方、同じページには評価、感想への導線、推薦など、本文とは更新頻度の違う情報も含まれている。

概念的には次のような状態になる。

エピソードHTML
├─ 小説本文             ほぼ不変
├─ タイトル             低頻度
├─ 前後リンク           低頻度
├─ 評価・リアクション   随時変化
├─ 感想関連             随時変化
└─ 推薦                 随時変化

2012年に公開された本文と、最近変化する情報が同じレスポンスの単位になり得る。

ページ単位でHTMLを組み立てる構成では珍しくないが、CDNや共有キャッシュを強く効かせる場合はキャッシュ寿命を決めにくくなる。

本文だけなら長くキャッシュできても、同じHTMLに短い寿命の情報が入れば、ページ全体のキャッシュ寿命をそちらへ引っ張られやすい。

オリジンまでリクエストが届くなら、入口で落とす意味は大きい

現在の内部構成は分からないが、仮に次のような経路なら、L7アクセス制御を強める理由は分かる。

User
 ↓
WAF / LB
 ↓
Web / Application
 ↓
アプリケーションキャッシュ
 ↓
DB

100万リクエストが来れば、WAFで落とさない限り後段へ流れる。

RedisのようなアプリケーションキャッシュがあればSQLへの到達は減らせる。

100万 requests
↓
Application 100万
↓
Redis 100万
↓
DBはキャッシュミスだけ

DBを守るうえでは有効でも、ApplicationやRedisまで100万回処理すること自体は変わらない。

このような構成を守るなら、入口で不要なアクセスを早く落とすことには意味がある。

問題は、そこでPrivate Relayを使う普通の読者まで巻き込む必要があるのか、という点だ。

ここからは、自分ならどう守るかという設計案

公開されている本文HTMLの性質を踏まえて、自分ならオリジン負荷をどう減らすかを考える。

まず匿名の本文ページをマイクロキャッシュする

全面的に作り直さなくても、匿名ユーザーの小説本文ページだけ数秒から数十秒キャッシュするところから試せる。

User
 ↓
CDN / Reverse Proxy
 ↓
マイクロキャッシュ
 ↓ キャッシュミスだけ
既存のページ生成処理

キャッシュされた後の同一ページへのアクセスはオリジンまで届かなくなる。

さらにCDNによっては、同じキャッシュキーに対する同時のキャッシュミスをまとめ、オリジンへの問い合わせを1つに近づけるRequest Collapsingも利用できる。CloudFrontにも同一オブジェクトへの同時リクエストをまとめる仕組みがある。

不要なQuery StringやCookieまでキャッシュキーへ含めると、同じ本文でも別オブジェクトとして扱われてヒット率が下がる。

本文の表示に不要な値をキャッシュキーから外す設計も必要になる。

次にコンテンツごとの寿命を決める

マイクロキャッシュで問題がなければ、どこまで長く保持できるかを設計する。

コンテンツキャッシュ方針の例
小説本文長いTTL、編集時にパージ
タイトル・前後リンク長め、作品更新時にパージ
作品トップ数分〜数十分、投稿時にパージ
推薦短TTL
感想一覧短TTL
ブックマーク・評価済み状態ユーザー単位、共有キャッシュから分離
投稿・編集動的処理

寿命が違うからといって、すべてを細かいAPIへ分割すればよいわけでもない。

HTTPリクエストを増やし過ぎれば、ブラウザ側のオーバーヘッドや実装の複雑さが増える。

例えば本文HTMLと、ユーザー固有状態を返す1本のAPIくらいからでも始められる。

更新時にパージする

作者が第10話を編集したとする。

作者が第10話を編集
↓
DB更新
↓
/n4402bd/10/ のキャッシュをパージ
↓
次回アクセスで再生成

これなら、普段は本文を長くエッジへ置いておける。

IDCFにもCDNサービスがあり、URLやSurrogate-Key単位のキャッシュ削除を利用できる。

IDCFを利用しているからエッジキャッシュを使えない、という話ではない。

必要なら本文を静的に書き出す

さらに進めるなら、小説本文は投稿・編集をイベントとしてHTMLを書き出す設計とも相性がいい。

作者が投稿・編集
↓
DB更新
↓
本文HTML生成
↓
Object Storage / CDN
↓
読者には静的配信

コメント、ブックマーク、評価などは別に扱う。

最初から全作品を完全静的化する必要はなく、

マイクロキャッシュ
↓
エッジキャッシュ
↓
更新時パージ
↓
必要なら静的書き出し

くらいの順序で試したい。

WAFだけにオリジン保護を依存しない

CDNの前後どこにWAFを置くかは製品や構成によって異なり、エッジ側でキャッシュヒットを含めてリクエストを検査する構成もある。

そのため、「キャッシュすればWAFを通らなくなる」という話ではない。

重要なのは、キャッシュ可能な本文GETをオリジンまで到達させないことだ。

User
 ↓
CDN / WAF
 ├─ キャッシュ済みの小説本文 → エッジで応答
 │
 └─ 動的処理・キャッシュミス
       ↓
      Application
       ↓
      Cache / DB

こうしてオリジンへ流すリクエストを減らせれば、L7のアクセス制御だけにオリジン保護を依存しなくて済む。

コメント投稿、ログイン、検索、評価、ブックマークなど、本当に動的処理が必要な部分には引き続きWAFやRate Limitを使える。

小説本文を100万回GETするbotに対しても、アクセス元を毎回細かく判定するだけでなく、「100万回来ても、その大半をエッジで返してオリジンへ届かせない」という防御が取れる。

それでもネットワーク単位のブロックには意味がある

エッジキャッシュがあればアクセス制御が不要になるわけではない。

悪質なbotを止めれば、

  • CDN自体のリクエスト量
  • ログ量
  • キャッシュミス
  • スキャンのノイズ
  • 動的APIへの攻撃
  • Bot Managementや後段処理のコスト

を減らせる。

ただ、正規ユーザーを広く巻き込むブロックなら話は別だ。

Private Relayは、誰でも匿名で使えるオープンProxyではない。

Appleは有効なAppleデバイスとiCloud+サブスクリプションを確認し、Private Relay側にも不正利用を抑える仕組みを持っている。Webサイト運営者には、Private Relayの共有IPを大規模なCGNATや企業ネットワークのように扱い、IPベース判定を調整するよう案内している。

そこで一般のSafari利用者まで403になるなら、「そのIPをどう判定するか」だけでなく、「本文GETをオリジンへ到達させずに済まないか」も考えたい。

読者から見れば、403の理由は関係ない

攻撃対策として成立しているかどうかと、読者体験は別の話だ。

読者には、

小説を開く
↓
403

としか見えない。

Private Relayが原因だと調べる人もいるだろうし、Chromeなら見られることに気付く人もいるだろう。

同じ作品が別のサービスにも掲載されていれば、読者がそちらへ移る可能性もある。

攻撃者には出口IPを変える選択肢がある一方、一般読者はサービスから離れるかもしれない。この非対称性は無視しにくい。

まとめ

今回の403がどのルールによるものなのかは外から確認できない。

ただ、HTTP 403という挙動と実際の出口IPごとの違いを見ると、帯域飽和型DDoSの主対策というより、L7 HTTP Flood、bot、scrapingなどに対するアクセス制御として考える方が近い。

IP、ASN、IP Reputation、Rate Limitを入口で使うことには意味がある。一方、Private Relayのように多数の正規ユーザーが共有する出口IPを強く制限すると、攻撃とは関係のない読者も巻き込む。

実際の小説ページでは本文が完成HTMLに含まれている。現在どのような共有キャッシュが使われているかは外から確認できないが、公開済みの小説本文そのものは読み取り中心で、キャッシュしやすい性質を持っている。

自分なら、一般ユーザーを含むネットワークを広く拒否する前に、まず匿名の本文ページをマイクロキャッシュする。そこからキャッシュ寿命とキャッシュキーを設計し、投稿・編集時のパージを追加し、必要なら本文の静的書き出しまで進める。

Private Relayを巻き込むアクセス制御にもL7対策としての意味はある。ただ、小説本文のようなコンテンツなら、アクセス元を広く拒否するだけでなく、オリジンへ届く本文GETそのものを減らす設計も並行して考えたい。