🌅

AWS|ノウハウ:Web Socket APIにIP制限をかける方法

に公開

知識は武器とかけまして、レゴブロックと解く、その心は――
今日もKnowledge Oasisへようこそ
案内人はkoふみです
本日のテーマは『Web Socket APIにIP制限をかける方法』

はじめに

「なんでREST APIでは簡単にIP制限できるのに、WebSocket APIではできないんだろう?」
つい先日、わたしもその疑問に頭を抱えました。調べてみると、AWSの設計上の仕様や制限が背景にあることが分かってきて……。そこで本記事では、初心者でもわかるように「なぜ制限できないのか」を丁寧に説明しつつ、実現方法をやさしく解説します。

対象読者

  • AWSをこれから使ってみたいという方
  • REST APIではIP制限できたのにWebSocket APIではできずに困っている方
  • 技術の背景と実践的な対応策、両方を理解したい方

RestAPIにIP制限をかける方法

以下の2つが、REST APIでのIP制限で用いられる代表的な手段です。

WAFを使う方法

AWS WAFをREST APIに適用すれば、IPアドレスやCIDR、特定のヘッダー・文字列などを基にアクセス制御できます。例えば「ホワイトリスト方式」で特定IPのみ許可し、それ以外を弾くことも可能です 。WAFはREST APIに直接紐づけでき、非常に簡単に強固なセキュリティを実現できます。

API Gatewayのリソースポリシーを設定する方法

REST APIの場合、API Gateway自体にリソースポリシーという仕組みがあり、ここでIPアドレスを条件として許可・拒否が可能です。たとえば、以下のようなポリシーで「特定IP以外を拒否」できます :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": [
                "execute-api:/*"
            ]
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": [
               "execute-api:/*"
            ],
            "Condition" : {
                "IpAddress": {
                    "aws:SourceIp": ["192.0.2.0/24", "198.51.100.0/24" ]
                }
            }
        }
    ]
}

Web Socket APIにIP制限がかけられない?

WAFで無理な理由

AWS WAFは、CloudFrontやALBなど特定のリソースにだけ紐づけ可能であり、API GatewayのWebSocket API(v2)には対応していません。そのため、REST APIのようにWAF単体でIP制限はできないのです。

API Gatewayのリソースポリシーで無理な理由

REST APIでは可能だったAPI Gatewayのリソースポリシーも、WebSocket APIには設定できません。WebSocket API(v2)にはリソースポリシー機能そのものが現状なく、このためREST APIと同じ方式ではIP制限が実現できないのです 。

Web Socket APIにIP制限をかける方法

CloudFront + WAF

CloudFrontはWebSocketにも対応しており、GlobalスコープのWAFと組み合わせることで、ハンドシェイク時(Upgradeリクエスト)にIP制限が可能です。具体的には以下の構成で実現できます:

  1. Web Socket APIに CloudFrontディストリビューションを作成 。オリジンにはWebSocket APIを指定します。
  2. Behaviorを作成。気を付ける設定は以下の通りです。
    1. Path Pattern: API Gatewayのステージに合わせたパス。/prod/* など。
    2. Viewer Protocol Policy: HTTPS Only
    3. Origin Request Policy: AllViewerExceptHostHeader
  3. GlobalスコープでIPSet(許可IP)とのWeb ACLを作成。 GlobalスコープのWAFしかCloud Frontを割り当てられません。
  4. Web ACLにIPSetルール(Allow)とデフォルトBlockルールを設定し、CloudFrontに関連付け。

これにより、CloudFrontのエッジでIPチェックが行われ、不正な接続はハンドシェイク前にブロックできます。
ただし、ハンドシェイク後のデータフレームには影響ありません。

注意点

/prod/*のパス設定を誤ってオリジンのOrigin Pathにしてしまわないこと。
Origin Pathに /hogeを設定していた場合、xxxxxxx.cloudfront.net/prod へのアクセスすると、 xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/hoge/prod に転送されます。
私は誤ってOrigin Pathに設定してしまい、Web Socket APIが呼び出せない沼にはまってしまい、抜け出すのに苦労しました。

Lambda オーソライザー

もう一つは $connect ルートにLambda REQUESTオーソライザーを設定する方法です。手順は以下の通り:

  1. $connect 統合用Lambda関数を作成。成功時にHTTP 200を返すだけでOK。接続確立が進みます。
    1. で作成した $connect 統合用Lambda関数を指定して、Web Socket APIに $connect ルートを追加。
  2. Lambda オーソライザー用Lambda関数を作成。環境変数で許可IPリストを保持。IPアドレスを判定して許可・拒否判定を行い、Allow/Denyポリシーを返します。
    1. で作ったLambda関数を指定してLambda オーソライザーを作成。IDソースタイプにはコンテキストのroute.request.context.identity.sourceIpを設定する。
  3. $connect ルートのルートリクエストの設定で 認可 に上記で作成したLamdaオーソライザーを指定する。

$connect統合用Lambdaの実装例

def lambda_handler(event, context):
    # (任意) 接続イベントをログや DB に保存
    conn_id = event['requestContext']['connectionId']
    print(f'Connected: {conn_id}', event['requestContext']['identity']['sourceIp'])
    # 成功レスポンス
    return { 'statusCode': 200 }

Lambdaオーソライザーの実装例

import os

def generate_policy(principal_id, effect, resource):
    """
    API Gateway Lambda オーソライザー用ポリシー生成ヘルパー
    """
    auth_response = {
        'principalId': principal_id,
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Action': 'execute-api:Invoke',
                'Effect': effect,
                'Resource': resource
            }]
        }
    }
    return auth_response

def lambda_handler(event, context):
    """
    Lambda REQUEST オーソライザー本体
    """
    # 環境変数からホワイトリスト IP を取得(カンマ区切り)
    allowed_ips = os.environ.get('ALLOWED_IPS', '')
    whitelist = {ip.strip() for ip in allowed_ips.split(',') if ip.strip()}

    # WebSocket 接続ハンドシェイク時のリクエスト情報
    ctx = event.get('requestContext', {})
    # IPアドレスを取得
    source_ip = ctx.get('identity', {}).get('sourceIp')

    # principalId は任意の識別子。IP をそのまま使っても OK
    principal_id = source_ip or 'unknown'

    # 接続先リソース
    method_arn = event.get('methodArn')

    if source_ip in whitelist:
        # 許可リストにあれば Allow
        return generate_policy(principal_id, 'Allow', method_arn)
    else:
        # なければ Unauthorized (403)
        # Lambda オーソライザーでは例外をスローすると 401 に、
        # policy で Block 相当のステートメントを返すと 403 になります。
        return generate_policy(principal_id, 'Deny', method_arn)

この方法ならAWSの他サービスに頼らずに済み、API層だけで完結しますが、Lambda起動時間(Cold Start)とコストが接続ごとにかかる点に注意が必要です。また接続後の許可状態はそのまま維持され、動的制御には別途ロジックが必要です。


比較

特徴 Lambda オーソライザー CloudFront + WAF
適用タイミング $connect ハンドシェイク時 Upgradeリクエスト(ハンドシェイク前)
実装のしやすさ API Gateway内で完結、手軽 構成が多く複雑
レイテンシ Cold Startで数十〜百ms遅延あり エッジで高速
コスト Lambda実行 + Gateway料金 CloudFront + WAF料金
スケーラビリティ Lambda同時実行制限あり 高トラフィックにも強い
制御の柔軟性 アプリケーションで動的制御可能 WAFルールに限られる

選び方の目安

  • 少人数の利用・すぐ試したい → Lambdaオーソライザが手軽
  • 大量接続・低遅延が求められる → CloudFront + WAFが安定

まとめ

REST APIに比べてWebSocket APIではWAFやリソースポリシーが使えないため、IP制限には別の工夫が必要です。$connectルートでLambdaオーソライザーを使う方法と、CloudFront+GlobalスコープのWAFを使ってエッジで制御する方法があります。それぞれメリット・デメリットもあるので、ご自身のユースケースに応じて選んでみてください。


知識のひとつひとつは小さなレゴブロック
でも、組み合わせれば世界を変えるアイディアをカタチにする武器になる!

またKnowledge Oasisでお会いしましょう
案内人はkoふみでした

Discussion