Skip to content
Published on

Ingress のレートリミットと DDoS 緩和: ingress-nginx から Gateway API までの実践ガイド

Authors

はじめに

ある日の早朝、普段は毎秒 200 件ほどを処理していたログイン API に、突然毎秒 4 万件のリクエストが押し寄せました。データベースのコネクションプールは一瞬で枯渇し、正常なユーザーは 504 Gateway Timeout に直面しました。トラフィックを追跡すると、数千の IP から同じエンドポイントへ同じペイロードが繰り返し送信されていました。典型的な L7 アプリケーション層攻撃でした。

こうした状況でまず思い浮かぶ防御線はアプリケーションコードです。しかしアプリケーションがリクエストを受け取ったということは、すでにコネクションが確立され、TLS ハンドシェイクが終わり、ワーカースレッドが占有されたということです。つまりコストはすでに発生しているのです。本当の防御は、リクエストが高価なリソースに到達する前、できるだけ外側で行われなければなりません。Kubernetes 環境では、その外側の境界がまさに Ingress 層です。

この記事では、Ingress 層でレートリミットをどう構成するか、分散環境で正確なカウントがなぜ難しいか、そして L3/L4 のボリューメトリック攻撃と L7 のアプリケーション攻撃をどのように異なる層で分離して防御するかを、実践的なコードとともに整理します。2026 年現在、Ingress API は事実上凍結(frozen)され、Gateway API が後継標準として定着しているため、両 API の関係とマイグレーションの観点も併せて確認していきます。

レートリミットの基本原理

なぜ L7 で制限するのか

ネットワーク防御は OSI 層ごとに役割が異なります。以下の表は、各層が何を見て何を防げるのかを整理したものです。

見える情報防げるもの防げないもの
L3 (IP)送信元・宛先 IPIP 単位の遮断、geo 遮断正常 IP を偽装したボットネット
L4 (TCP/UDP)ポート、コネクション状態SYN フラッド、コネクション数制限HTTP パス単位の細かい制御
L7 (HTTP)メソッド、パス、ヘッダー、クッキーエンドポイント別 RPS、ユーザー別クォータ大容量ボリューメトリックフラッド

要点は、各層が最も得意とするものを防ぐべきという点です。毎秒数百ギガビットのボリューメトリック攻撃を L7 Ingress で防ごうとすると、パケットがクラスタネットワークまで入り込み帯域を食いつぶした後でようやく遮断されます。逆に、特定のログインエンドポイントに毎秒 5 回制限といった細かいルールは、L3/L4 機器では表現できません。

トークンバケットとリーキーバケット

レートリミットアルゴリズムの二大柱はトークンバケット(token bucket)とリーキーバケット(leaky bucket)です。

[トークンバケット]                    [リーキーバケット]

 補充レート r ──> ( バケット容量 b )    リクエスト ──> ( キュー容量 b ) ──> 流出レート r ──> 処理
              │                                  │
   リクエスト到着時にトークン 1 個消費    キューが満杯ならリクエスト破棄(drop)
   トークンがなければ拒否(reject)        一定速度でのみ流出する
  • トークンバケット: 平均速度 r を維持しつつ、バケット容量 b 分の瞬間バーストを許容します。短いスパイクを自然に吸収します。
  • リーキーバケット: 出力速度を厳格に平滑化します。バーストを吸収せず、一定速度でのみ流出させます。

ingress-nginx のレートリミットは内部的に nginx の limit_req(リーキーバケットに近いモデル)と limit_conn を使います。burst パラメータで少しのバーストを許容する方式です。

ingress-nginx アノテーション詳解

ingress-nginx は Ingress リソースのアノテーションでレートリミットを宣言します。最もよく使われる 4 つを整理します。

アノテーション意味単位
limit-rps毎秒の許容リクエスト数requests per second
limit-rpm毎分の許容リクエスト数requests per minute
limit-connections同時コネクション数の制限connections
limit-burst-multiplierburst 許容倍率(デフォルト 5)multiplier

次はログインエンドポイントを保護する Ingress の例です。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: auth-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "5"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "2"
    nginx.ingress.kubernetes.io/limit-connections: "10"
    # 信頼できる内部レンジは制限から除外
    nginx.ingress.kubernetes.io/limit-whitelist: "10.0.0.0/8,192.168.0.0/16"
spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /auth/login
            pathType: Prefix
            backend:
              service:
                name: auth-service
                port:
                  number: 8080

ここで limit-rps: 5limit-burst-multiplier: 2 を併用すると、実際の burst 容量は毎秒 10 件まで許容されます。つまり平均 5 RPS を維持しつつ、瞬間的には 10 件まで通過させ、それ以上は 503 Service Temporarily Unavailable で拒否します。

キー(key)は何で取られるのか

ingress-nginx のデフォルトのレートリミットキーはクライアント IP です。しかしプロキシ・ロードバランサーの背後にいる場合は、実際のクライアント IP を正確に識別する必要があります。このとき鍵となる設定が ConfigMap の信頼設定です。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  # 信頼するプロキシレンジ(例: クラウド LB の CIDR)
  proxy-real-ip-cidr: "130.211.0.0/22,35.191.0.0/16"
  use-forwarded-headers: "true"
  compute-full-forwarded-for: "true"

use-forwarded-headers を有効にすると、X-Forwarded-For ヘッダーの IP をクライアントとして使います。ただしこのヘッダーはクライアントが偽造できるため、必ず信頼レンジ(proxy-real-ip-cidr)を併せて指定する必要があります。そうしないと、攻撃者がリクエストごとに偽の X-Forwarded-For 値を入れてカウンターを回避できます。

グローバル上限とカスタムレスポンス

すべての Ingress に共通で適用するデフォルト上限は ConfigMap で設定します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  limit-rate: "0"                 # レスポンス帯域制限(バイト/秒)、0 は無制限
  limit-req-status-code: "429"    # デフォルトの 503 の代わりに 429 Too Many Requests を返す
  limit-conn-status-code: "429"

レートリミット拒否時のデフォルトレスポンスコードは 503 ですが、クライアントがバックオフ(backoff)を明確に認識できるよう 429 Too Many Requests に変えるのが一般的です。可能であれば Retry-After ヘッダーも併せて返すのが望ましいです。

Traefik / Kong / APISIX の比較

ingress-nginx が最も広く使われますが、運用環境や要件によって別のコントローラーを選ぶこともあります。レートリミットの観点で比較すると以下のようになります。

項目ingress-nginxTraefikKongAPISIX
設定方式アノテーションMiddleware CRDPlugin (KongPlugin)Plugin / Route
アルゴリズムリーキーバケット系スライディングウィンドウ平均固定/スライディングウィンドウトークン/リーキーバケットなど多様
分散カウントデフォルト非対応(per-pod)デフォルト非対応Redis 対応Redis クラスタ対応
キーのカスタマイズ限定的ソース基準consumer/IP/header変数ベースで柔軟
拒否レスポンスコード設定可能設定可能429 デフォルト設定可能

Traefik Middleware

Traefik は Middleware CRD でレートリミットを宣言します。

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: api-ratelimit
  namespace: production
spec:
  rateLimit:
    average: 100      # 平均毎秒 100 リクエスト
    burst: 50         # 瞬間 50 バーストを許容
    period: 1s
    sourceCriterion:
      ipStrategy:
        depth: 1      # X-Forwarded-For の末尾から 1 番目の IP を使う

Kong Plugin

Kong は KongPlugin リソースで宣言し、Redis をバックエンドに指定すると分散カウントが可能になります。

apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: rate-limiting-redis
  namespace: production
plugin: rate-limiting
config:
  minute: 60
  policy: redis
  redis_host: redis.production.svc.cluster.local
  redis_port: 6379
  fault_tolerant: true

APISIX Route

APISIX は limit-req、limit-conn、limit-count プラグインを提供し、Redis クラスタを通じたグローバルカウントをサポートします。

apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: api-route
  namespace: production
spec:
  http:
    - name: limited
      match:
        hosts:
          - api.example.com
        paths:
          - /api/*
      backends:
        - serviceName: api-service
          servicePort: 8080
      plugins:
        - name: limit-count
          enable: true
          config:
            count: 200
            time_window: 60
            rejected_code: 429
            policy: redis
            redis_host: redis.production.svc.cluster.local
            redis_port: 6379

分散環境のカウント問題

ここからが実務で最も足を引っ張られる地点です。Ingress コントローラーは通常、複数のポッド(replica)で稼働しています。各ポッドが自分のメモリにカウンターを持っていると、全体の上限は意図した値の倍数になってしまいます。

                          [ クライアントトラフィック ]
                ┌─────────────────┼─────────────────┐
                ▼                 ▼                 ▼
        ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
        │ ingress pod1 │  │ ingress pod2 │  │ ingress pod3 │
        │ メモリカウンター│  │ メモリカウンター│  │ メモリカウンター│
        │  上限 100      │  │  上限 100      │  │  上限 100      │
        └──────────────┘  └──────────────┘  └──────────────┘

  意図した上限: 100 RPS   →   実際の許容: 最大 300 RPS (ポッド数 x 100)

In-memory 方式の特性を整理すると以下のようになります。

方式精度レイテンシ障害耐性運用の複雑さ
In-memory (per-pod)低い(ポッド数だけ倍増)最速高い低い
Redis 中央カウンター高いネットワーク 1 ホップ追加Redis 障害に脆弱高い

ingress-nginx のデフォルト実装は per-pod メモリ方式なので、上限を設定する際は レプリカ数で割って 設定すると意図したグローバル上限に近づきます。例えばグローバル 300 RPS を望み、ポッドが 3 個ならポッドあたり 100 RPS に設定する形です。ただし HPA でポッド数が変わるとグローバル上限も追随して変わる点に注意が必要です。

正確なグローバルカウントが必要なら、Redis のような中央ストアをバックエンドに使う Kong、APISIX を選ぶか、ingress-nginx の前段に別途レートリミットゲートウェイを置く構成を検討します。Redis を使う際は次の設定が重要です。

- アトミックカウント: INCR + EXPIRE または Lua スクリプトで race condition を排除
- fault tolerant モード: Redis 障害時にリクエストを止めるか(fail-closed)通すか(fail-open)決める
- キー有効期限: ウィンドウ単位でキーが正確に失効するようスライディング/固定ウィンドウを選択
- レイテンシ予算: Redis 往復が p99 レイテンシに与える影響を計測

fail-open と fail-closed はトレードオフです。Redis が落ちたとき、fail-open は可用性を守るものの攻撃に晒され、fail-closed は安全ですが正常トラフィックまで止めます。ログイン・決済のような機微なエンドポイントは fail-closed、一般的な参照 API は fail-open を推奨します。

ホワイトリストと地域(geo)制御

すべてのトラフィックを同じように制限してはいけません。信頼できるパートナー、内部モニタリング、ヘルスチェックは除外すべきです。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: partner-api
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "20"
    # ホワイトリストレンジはレートリミットを回避
    nginx.ingress.kubernetes.io/limit-whitelist: "203.0.113.0/24,198.51.100.10/32"
spec:
  ingressClassName: nginx
  rules:
    - host: partner.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: partner-service
                port:
                  number: 80

地域遮断は GeoIP モジュールで実装します。ingress-nginx は nginx の GeoIP2 モジュールを通じて国コードベースの制御をサポートします。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  use-geoip2: "true"
  # 特定の国だけ許可または遮断するポリシーは server-snippet で記述

ただし GeoIP 遮断は VPN・プロキシで容易に回避されるため、単独防御ではなく補助シグナルとして活用するのが現実的です。正常なユーザーがまったくいない地域から来る攻撃的トラフィックを 1 次的にふるい落とす用途に適しています。

L3/L4 DDoS と L7 防御の分離

最もよくある設計ミスは、すべての DDoS を Ingress 一か所で防ごうとすることです。ボリューメトリック攻撃(L3/L4)とアプリケーション攻撃(L7)は防御位置そのものが異なるべきです。

[インターネット]
┌──────────────────────────────────────────────┐
│  クラウドエッジ (CDN / Anycast / スクラビング)    │  <- L3/L4 ボリューメトリック防御
│  - SYN フラッド、UDP アンプ、大容量パケットを吸収    │     (AWS Shield, Cloud Armor など)
└──────────────────────────────────────────────┘
   │  (正常に近いトラフィックのみ通過)
┌──────────────────────────────────────────────┐
│  Ingress 層 (ingress-nginx / Gateway)          │  <- L7 アプリケーション防御
│  - エンドポイント別 RPS、コネクション制限、WAF ルール │     (レートリミット、ボット遮断)
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│  アプリケーション (サービス/ポッド)                │  <- ビジネスロジッククォータ
│  - ユーザー別クォータ、冪等性、サーキットブレーカー    │
└──────────────────────────────────────────────┘

各層の役割分担を表に整理すると以下のようになります。

攻撃タイプ防御位置ツール
ボリューメトリック (L3/L4)SYN フラッド、UDP アンプクラウドエッジShield, Cloud Armor, スクラビング
プロトコル (L4)コネクション枯渇、slowlorisエッジ + Ingressコネクション制限、タイムアウト
アプリケーション (L7)HTTP フラッド、キャッシュバスティングIngress + アプリレートリミット、WAF

クラウドエッジでボリューメトリックを吸収すれば、Ingress まで到達するトラフィックはすでにかなり精製された状態です。この段階で Ingress は L7 ルールだけに集中できます。逆にエッジ保護なしで Ingress 単独で数百ギガビットを受けると、ノードの NIC 帯域と conntrack テーブルが先に崩れます。

slowloris のような低速攻撃は別途注意が必要です。少ない帯域でコネクションだけを長く占有する攻撃なので、次のようにタイムアウトを短く設定して防御します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  client-header-timeout: "10"
  client-body-timeout: "10"
  keep-alive-requests: "100"
  worker-shutdown-timeout: "30s"

ボットトラフィック対応

DDoS の相当数はボットネットが引き起こします。ボットと正常ユーザーを区別するシグナルはいくつかあります。

[ボット識別シグナル]
  - User-Agent の欠落または異常なパターン
  - クッキー/セッションを維持しないステートレスな繰り返しリクエスト
  - JavaScript チャレンジを通過できない
  - TLS フィンガープリント(JA3/JA4)が既知のボットツールと一致
  - 異常に均一なリクエスト間隔(人間にはジッターがある)

Ingress 層では、単純な User-Agent 遮断やヘッダー検証程度が可能です。server-snippet で空の User-Agent を拒否する例です。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: bot-protect
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/server-snippet: |
      if ($http_user_agent = "") {
        return 403;
      }
      if ($http_user_agent ~* "(curl|wget|python-requests|scrapy)") {
        return 403;
      }
spec:
  ingressClassName: nginx
  rules:
    - host: www.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-service
                port:
                  number: 80

ただし User-Agent は簡単に偽造されるため、精巧なボットには効果が限定的です。JavaScript チャレンジや JA3/JA4 フィンガープリンティングのような高度なボット管理機能は通常 CDN・WAF 層(クラウドエッジ)で処理し、Ingress は 1 次フィルターの役割を担うのが合理的です。

負荷シナリオで見る動作

設定値を決める際は、具体的なシナリオで検証する必要があります。以下は普段 200 RPS、上限 300 RPS(burst 含め 600)の API に 3 つの状況が発生したときの動作です。

シナリオ入力トラフィック通過拒否(429)ユーザー体感
正常ピーク280 RPS2800正常
マーケティングスパイク550 RPS (10 秒)約 600(バースト吸収後に平滑化)一部わずかに遅延
L7 フラッド40000 RPS30039700正常ユーザーは保護される

3 つ目のシナリオがレートリミットの存在理由です。4 万 RPS が入ってきてもバックエンドには上限分だけ伝わり、残りは Ingress で 429 として切り落とされるので、データベースとアプリケーションは保護されます。拒否されたリクエストの処理コストは、バックエンド処理コストに比べて無視できるほど小さいです。

負荷テストは実際のデプロイ前に必ず実施します。次は簡単な検証コマンドの例です。

# hey で同時 200、合計 20000 リクエストを発射
hey -z 30s -c 200 https://api.example.com/api/items

# 拒否率を確認するためレスポンスコード分布を集計
hey -z 30s -c 500 https://api.example.com/auth/login \
  | grep -A20 "Status code distribution"

Gateway API との関係

2026 年現在、Ingress API は新機能追加が止まった凍結状態であり、後継標準は Gateway API です。Gateway API は役割分離(インフラ運用者 / クラスタ運用者 / アプリ開発者)、プロトコル拡張性、表現力のあるルーティングを目標に設計されています。

観点IngressGateway API
標準の状態凍結 (frozen)活発に発展中
リソースIngress 単一GatewayClass / Gateway / HTTPRoute など
役割分離弱い明示的(RBAC フレンドリー)
レートリミット実装のアノテーションポリシーアタッチメント + 実装拡張
トラフィック分割アノテーション重みベースの標準フィールド

ただしレートリミットは Gateway API の標準仕様に完全には含まれておらず、実装ごとのポリシー(policy attachment)や拡張フィルターで提供されることが多いです。したがってマイグレーション時には、使っている実装がレートリミットをどの方式で公開しているかを確認する必要があります。

ingress-nginx は 2025 年を境に事実上メンテナンス(maintenance)モードに入り、セキュリティ脆弱性対応中心に運用される傾向です。新規クラスタを設計するなら、Gateway API ベースの実装(例: Envoy Gateway、Traefik の Gateway 対応、Cilium Gateway など)を優先的に検討するのが長期的に安全です。次は簡単な HTTPRoute の例です。

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-route
  namespace: production
spec:
  parentRefs:
    - name: prod-gateway
  hostnames:
    - api.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-service
          port: 8080

運用とチューニング

可観測性の確保

レートリミットは有効にして終わりではありません。どれだけ拒否されているか、拒否されたトラフィックが攻撃なのか正常ユーザーなのかを絶えず観測する必要があります。

[観測指標]
  - 429/503 レスポンス比率(全体に対する)
  - 拒否されたリクエストの IP/パス/User-Agent 分布
  - バックエンドの p50/p95/p99 レイテンシ変化
  - Ingress ポッドの CPU/メモリ/コネクション数
  - upstream 5xx 比率(上限が効果的か判断)

Prometheus で ingress-nginx メトリクスを収集すれば、nginx_ingress_controller_requests の status ラベルで 429 比率を追跡できます。拒否率が急に跳ね上がれば、攻撃か、上限が厳しすぎるかのどちらかです。

段階的な適用

レートリミットを本番にいきなり強くかけると、正常ユーザーが遮断されるリスクがあります。次の順序を推奨します。

1. 観測モード: 上限を非常に高く設定し拒否なしでメトリクスのみ収集
2. ベースライン把握: 普段のピーク RPS、p99、正常なバーストパターンを分析
3. 保守的な適用: 普段のピークの 3〜5 倍を上限に設定
4. 漸進的な強化: 拒否率とユーザー影響を見ながら上限を調整
5. エンドポイント差別化: ログイン/決済は厳格、静的参照は緩く

よくある落とし穴とトラブルシューティング

実務で繰り返し遭遇する落とし穴を整理します。

[落とし穴 1] X-Forwarded-For 未設定
  → すべてのリクエストが LB の IP 1 つにカウントされ全体が一斉に遮断される
  → proxy-real-ip-cidr と use-forwarded-headers を必ず併せて設定

[落とし穴 2] per-pod カウントをグローバルと誤解
  → 上限 100 なのにポッド 3 個で実際は 300 まで許容される
  → レプリカ数で割って設定するか中央カウンター(Redis)を導入

[落とし穴 3] ヘルスチェック/プローブがレートリミットに引っかかる
  → kubelet プローブやモニタリングが 429 を受けてポッドが落ちる
  → 内部レンジを whitelist に追加

[落とし穴 4] 拒否コードを 503 のまま放置
  → クライアントがサーバー障害と誤認し無限リトライ → 悪循環
  → 429 + Retry-After でバックオフを誘導

[落とし穴 5] L7 でボリューメトリックを防ごうとする
  → パケットがノードまで入り NIC/conntrack が先に飽和
  → ボリューメトリックはクラウドエッジで吸収

[落とし穴 6] 上限を低く設定しすぎる
  → 正常ピークでも 429 が発生しユーザー離脱
  → 観測モードでベースラインからまず計測

デバッグコマンド

問題が疑われるときに素早く確認できるコマンドです。

# Ingress コントローラーのログから limiting 関連メッセージを確認
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller | grep -i limit

# 実際に適用された nginx 設定を確認(生成された limit_req ゾーンを検証)
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
  cat /etc/nginx/nginx.conf | grep -A3 limit_req_zone

# 特定 IP から見えるレスポンスコード分布を素早く点検
for i in $(seq 1 20); do
  curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/auth/login
done | sort | uniq -c

運用チェックリスト

デプロイ前の最終点検リストです。

[ ] エンドポイント別に上限を差別化して設定したか(ログイン厳格、参照緩く)
[ ] X-Forwarded-For 信頼レンジ(proxy-real-ip-cidr)を正確に指定したか
[ ] 拒否レスポンスを 429 + Retry-After に設定したか
[ ] ヘルスチェック/モニタリング/内部レンジを whitelist に入れたか
[ ] per-pod vs Redis カウント方式を意図どおり選択したか
[ ] レプリカ数変動(HPA)がグローバル上限に与える影響を考慮したか
[ ] L3/L4 ボリューメトリック防御をクラウドエッジに配置したか
[ ] slowloris 対策にタイムアウトを短く設定したか
[ ] 429/503 比率とバックエンドレイテンシをダッシュボードで観測中か
[ ] 負荷テストで上限の動作を事前検証したか
[ ] Gateway API マイグレーション経路を検討したか

おわりに

レートリミットと DDoS 緩和の核心は、層ごとに適切な位置で適切な攻撃を防ぐことです。ボリューメトリック攻撃はクラウドエッジで、アプリケーション層の濫用は Ingress で、ビジネスルール違反はアプリケーションで防ぐべきです。どこか一つの層にすべての責任を寄せれば必ず崩れます。

分散環境のカウント問題は特に静かに人を欺きます。per-pod メモリカウントをグローバル上限と勘違いすると、設定した値の数倍までトラフィックが漏れ出ます。正確性が重要なら中央カウンターを導入しつつ、Redis 障害時の fail-open/fail-closed ポリシーを明確に定めておく必要があります。

最後に、2026 年の方向性は明確です。Ingress API は凍結され、ingress-nginx はメンテナンスモードに入り、Gateway API が後継標準として定着しつつあります。今運用中の ingress-nginx 設定をしっかり整えつつ、新規設計では Gateway API ベースの実装とそのレートリミットモデルを積極的に検討されることをお勧めします。

参考資料