上級 40分 Lesson 22

Lambda & API Gateway — 実行ロール・認証方式・mTLS

Lambda実行ロール/VPC設計、API Gateway認証3方式、リソースポリシー、mTLSを徹底解説

AWS Lambda API Gateway SCS-C03 サーバーレス Security

イントロダクション

Lambda と API Gateway は現代的なAWSアーキテクチャの中核をなすサーバーレスコンポーネント。しかしセキュリティの複雑さは見た目以上だ。

実行ロール、リソースポリシー、認証方式の選択——これらはすべて独立ではなく相互に作用する。本講では、SCS-C03 で頻出する設計パターンと落とし穴を徹底解説する。

API Gateway 認証3方式の比較フロー

Loading diagram...

Lambda VPC 接続アーキテクチャ

Loading diagram...


第1部: Lambda セキュリティ

1. 実行ロール(Execution Role)— 最小権限設計の基本

実行ロールの役割

Lambda関数が実行時に引き受けるIAMロール。S3へのアクセス、DynamoDB書き込み、ログ出力など、関数が「自分で」リソースにアクセスするときの権限を定義する。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/my-function:*"
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/input/*"
    },
    {
      "Effect": "Allow",
      "Action": "dynamodb:PutItem",
      "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/my-table"
    }
  ]
}

リソースベースポリシーとの違い

観点実行ロールリソースベースポリシー
何を制御するかLambdaが「できること」「誰がLambdaを呼べるか」
ポリシー定義の位置IAMロールに付与Lambda関数リソースに直接付与
クロスアカウント対応信頼ポリシー(Trust Policy)が必要直接指定可能
同一アカウント内通信通常は実行ロールのみで十分不要(通常)

クロスアカウント呼び出しの例

アカウントA内のEC2がアカウントB内のLambdaを呼ぶ場合:

// アカウントB: Lambda のリソースベースポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:role/ec2-role"  // アカウントA
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:222222222222:function:my-function"
    }
  ]
}

さらに、アカウントA内のEC2ロール自体が以下の信頼ポリシーを持つ必要はない(Lambda呼び出しはリソースベースで許可されているため)。

2. リソースベースポリシー — 「誰が呼べるか」を制御

API Gateway からの呼び出し

API Gatewayがログを出力するには、Lambda関数のリソースベースポリシーで許可される必要はない(CloudWatch Logsロールで制御される)。しかし、API GatewayがLambdaを呼ぶことは許可が必要。

AWS マネージドポリシー AWSLambdaFullAccess を使わず、APIGatewayからの呼び出しのみ許可する:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:my-function",
      "Condition": {
        "ArnLike": {
          "aws:SourceArn": "arn:aws:execute-api:ap-northeast-1:123456789012:abcdef123/*/POST/users"
        }
      }
    }
  ]
}

S3 イベント トリガー

S3バケットからのLambda呼び出しも同様:

{
  "Effect": "Allow",
  "Principal": {
    "Service": "s3.amazonaws.com"
  },
  "Action": "lambda:InvokeFunction",
  "Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:s3-processor",
  "Condition": {
    "ArnLike": {
      "aws:SourceArn": "arn:aws:s3:::my-bucket"
    }
  }
}

試験で狙われるポイント

  • リソースベースポリシーは呼び出し元ごとに制御可能(ワイルドカード回避)
  • IAM実行ロール + リソースベースポリシーの両方が「許可」である必要がある(AND条件)
  • 見落としやすい: Lambda実行ロールにCloudWatch Logsのputイベント権限がないと、アクティビティログすら出力されない

3. VPC接続Lambda — ENI、NAT Gateway、セキュリティグループ

VPC接続の仕組み

Lambda関数がVPC内のリソース(RDS、Elasticacheなど)にアクセスするには、VPCに接続する必要がある。この時、AWSが自動的にENI(Elastic Network Interface)をプライベートサブネットに作成する。

# Lambda 設定(例)
VpcConfig:
  SubnetIds:
    - subnet-12345678  # プライベートサブネット(NAT Gateway経由でインターネット接続)
    - subnet-87654321  # AZ冗長化
  SecurityGroupIds:
    - sg-lambda

NAT Gateway の必須性

VPC内のLambdaがインターネットアクセスが必要な場合(例:外部APIを呼ぶ、S3にアップロード)、以下が必須:

  1. NAT Gateway をパブリックサブネットに配置
  2. プライベートサブネットのルートテーブルで 0.0.0.0/0 をNAT Gatewayに向ける
  3. 無料枠なし — NAT Gateway は時間単位 + データ転送で課金

VPC接続がない場合:

VpcConfig: {}  # または、このセクション自体を省略

この場合、Lambda はデフォルトVPCのIAMロール権限でのみ動作し、パブリックインターネットアクセスは可能だが、VPC内プライベートリソースへのアクセスは不可

VPC エンドポイント活用(コスト削減)

NAT Gateway を使わずに AWS サービスへアクセス:

# VPC エンドポイント (Gateway) — S3, DynamoDB は無料
- Type: AWS::EC2::VPCEndpoint
  Properties:
    VpcId: vpc-12345
    ServiceName: com.amazonaws.ap-northeast-1.s3
    RouteTableIds:
      - rtb-lambda

# VPC エンドポイント (Interface) — API Gateway 等
- Type: AWS::EC2::VPCEndpoint
  Properties:
    VpcId: vpc-12345
    ServiceName: com.amazonaws.ap-northeast-1.apigateway
    PrivateDnsEnabled: true
    SubnetIds:
      - subnet-lambda
    SecurityGroupIds:
      - sg-vpce

セキュリティグループ設定:

// sg-vpce のインバウンド
{
  "IpProtocol": "tcp",
  "FromPort": 443,
  "ToPort": 443,
  "SourceSecurityGroupId": "sg-lambda"
}

これにより、Lambda は NAT Gateway コスト(月 $32 以上)を回避して AWS サービスに接続できる。


4. 環境変数の暗号化 — KMS キーの選択

デフォルト暗号化 vs カスタム KMS キー

Lambda環境変数は デフォルトでAWS管理キーで暗号化 される。

方式キー管理者キー操作権限コスト使用場面
デフォルト暗号化AWSユーザーが制御不可無料非機密データ、開発環境
カスタムKMSキーユーザーIAMで制御$1/月 + 使用料APIキー、データベース接続文字列、本番環境

カスタム KMS キー設定

import boto3
import json

# Lambda関数内で、カスタムキーで暗号化された環境変数を復号
kms = boto3.client('kms')

def decrypt_env_var(encrypted_value):
    """KMSで暗号化された環境変数を復号(第1回呼び出しでキャッシュされる)"""
    if encrypted_value.startswith('aws/lambda'):  # デフォルトキー
        # CloudWatch Logsに出力される際、自動的に復号される
        return encrypted_value
    
    response = kms.decrypt(
        CiphertextBlob=bytes.fromhex(encrypted_value),
        EncryptionContext={
            'aws:lambda:FunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME'],
            'aws:lambda:alias': 'LIVE'
        }
    )
    return response['Plaintext'].decode('utf-8')

def lambda_handler(event, context):
    db_password = decrypt_env_var(os.environ['DB_PASSWORD'])
    # 以下のロジック...

IAM ポリシー(Lambda実行ロール)

{
  "Effect": "Allow",
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/12345678-1234-1234-1234-123456789012",
  "Condition": {
    "StringEquals": {
      "kms:EncryptionContext:aws:lambda:FunctionName": "my-function"
    }
  }
}

注意: CloudWatch Logsに環境変数がログアウトされる場合、デフォルトでは復号されない。別途マスキング設定が必要:

{
  "LogGroupName": "/aws/lambda/my-function",
  "LogGroups": [
    {
      "LogGroupName": "/aws/lambda/my-function",
      "KmsKeyArn": "arn:aws:kms:ap-northeast-1:123456789012:key/..."
    }
  ]
}

5. レイヤーのセキュリティ — 共有コードの信頼性

Lambda レイヤーは複数の関数間で共有できる非常に便利な機構だが、セキュリティ上の落とし穴がある。

レイヤーのリソースベースポリシー

公開されたレイヤーは 他のAWSアカウントから利用可能 になる:

# レイヤーのアクセス許可確認
aws lambda get-layer-version-policy \
  --layer-name my-shared-layer \
  --version-number 1

レイヤーを特定のアカウントのみに限定:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"  // 同一アカウント
      },
      "Action": "lambda:GetLayerVersion",
      "Resource": "arn:aws:lambda:ap-northeast-1:111111111111:layer:my-layer:1"
    }
  ]
}

レイヤー内の依存関係の検証

レイヤーに含まれるライブラリの脆弱性を定期的にスキャン:

# レイヤーに含まれるPythonライブラリをスキャン
pip install safety
safety check --file requirements.txt

# または
npm audit  # Node.js の場合

CI/CD パイプラインでレイヤー更新前に実行:

# GitHub Actions 例
- name: Scan layer dependencies
  run: |
    pip install -r layers/common/python/requirements.txt
    safety check --file layers/common/python/requirements.txt

6. 予約済み同時実行数(Reserved Concurrency)— DDoS保護

予約済み同時実行数の役割

Lambda の同時実行数に対して ハード制限 を設ける:

# 同時実行数を10に制限
aws lambda put-function-concurrency \
  --function-name my-function \
  --reserved-concurrent-executions 10

この場合:

  • 同時実行数が10を超えると、追加リクエストは ThrottlingException で拒否
  • 他のLambda関数の予約済み同時実行を圧迫しない(アカウント内の独立した上限)

スケーリング戦略

パターン予約済み同時実行数用途
Web API100通常のトラフィック許容範囲
クリティカルシステム50-200SLA厳守が必須
バッチ処理5-20時間がかかるため、並行度を低く
DDoS標的10攻撃時の連鎖効果を最小化

プロビジョニング済み同時実行数との違い

タイプ実行準備コスト用途
予約済みコールドスタート可能性あり無料(制限のみ)制御・保護
プロビジョニング済み常に準備完了(コールドスタートなし)$0.015/時間/インスタンス低遅延が必須

7. CodeSigningConfig — コード署名と改ざん防止

コード署名の設定

Lambda関数がデプロイされる際、コードが署名されていることを強制:

# Signer 証明書を作成(AWS Signer)
aws signer put-signing-profile \
  --profile-name my-profile \
  --signature-version SHA256withRSA

# Lambda関数にコード署名設定を適用
aws lambda put-code-signing-config \
  --function-name my-function \
  --code-signing-config-arn arn:aws:lambda:ap-northeast-1:123456789012:code-signing-config:csc-1234567890abcdef0

# 署名されていないコードをアップロードしようとするとエラー
# error: Invalid or missing code signing configuration

IAM ポリシー(署名権限)

{
  "Effect": "Allow",
  "Action": [
    "signer:GetSigningProfile",
    "signer:GetRevocation",
    "signer:ListSigningProfiles",
    "signer:DescribeSigningJob"
  ],
  "Resource": "*"
}

Use Case: 本番環境では全てのLambda関数にコード署名を強制し、未検証なコードの実行を防ぐ。


8. Lambda SnapStart — 初期化のセキュリティ考慮事項

SnapStart は 関数の初期化状態をスナップショット して、コールドスタート時間を短縮する(Java/Go)。

セキュリティ上の注意点

  1. スナップショット内に機密情報が含まれていないか確認

    // 危険: スナップショット時にAPIキーをメモリに保持
    public class LambdaHandler {
        private static String API_KEY = System.getenv("API_KEY");  // スナップショット時に読み込まれる
        
        public void handleRequest(Event event, Context context) {
            // API_KEY がスナップショット内に保存される可能性
        }
    }

    改善: 関数起動時に環境変数を読み込む

    public class LambdaHandler {
        private String apiKey;
        
        @Override
        public void init() {
            apiKey = System.getenv("API_KEY");  // 毎回読み込む
        }
    }
  2. データベース接続の状態管理

    // スナップショット時のDB接続が古い可能性
    private static Connection dbConn = DriverManager.getConnection(dbUrl);
    
    // 改善: SnapStart復帰時に再接続
    @SnapStartHandler
    public void restoreConnection() {
        if (dbConn != null && dbConn.isClosed()) {
            dbConn = DriverManager.getConnection(dbUrl);
        }
    }

9. /tmp ディレクトリの暗号化

Lambda は 一時的なファイルストレージとして /tmp を提供 する(512MB)。

暗号化設定

デフォルトでは暗号化されないため、機密ファイルは避けるべき:

import os
import tempfile
from cryptography.fernet import Fernet

# 不安全: /tmp にAPIキーを平文で保存
def unsafe_example():
    with open('/tmp/api_key.txt', 'w') as f:
        f.write(os.environ['API_KEY'])  # 危険!

# 安全: /tmp にはダウンロードした公開ファイルのみ
def safe_example():
    key = Fernet.generate_key()
    cipher = Fernet(key)
    
    # メモリ内で処理、/tmp は避ける
    encrypted_data = cipher.encrypt(b'sensitive')
    
    # または、Secrets Manager から読む
    import boto3
    secrets = boto3.client('secretsmanager')
    secret = secrets.get_secret_value(SecretId='my-secret')

10. Lambda の制限 — できないことを理解する

実行時間上限: 15分

import time

def lambda_handler(event, context):
    for i in range(1000):
        time.sleep(1)  # 15分後にタイムアウト(LambdaTimeoutException)

代替案:

  • 長時間処理: Step Functions で調整
  • バッチ: SQS + Lambda polling(連鎖実行)

メモリ上限: 10GB

10GB を超えるメモリが必要な場合:

  • EC2, ECS, Fargate を検討
  • GlueJob(大容量データ処理)

コールドスタートのセキュリティ影響

コールドスタート中は実行ロール認証が新規取得される。KMS暗号化キーにアクセスする場合、遅延が増加:

通常: 10ms → KMS キー復号で +50ms

改善: プロビジョニング済み同時実行数を使用し、コールドスタートを回避。


第2部: API Gateway セキュリティ

11. 認証方式の比較 — IAM vs Cognito vs Lambda オーソライザー

11.1 IAM 認証

API が AWS IAM ユーザー・ロール でのみアクセス可能な場合(内部API)。

import boto3
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

# Python クライアント例
session = boto3.Session()
credentials = session.get_credentials()

url = 'https://api.example.com/resource'
request = AWSRequest(method='GET', url=url)
SigV4Auth(credentials, 'execute-api', 'ap-northeast-1').add_auth(request)

response = requests.get(url, headers=dict(request.headers))

メリット:

  • AWS内の統一認証
  • CloudTrail で追跡可能
  • 複数の認証を組み合わせ不要

デメリット:

  • モバイルアプリ・SPA には不向き(認証情報の管理が複雑)
  • 署名プロセスが必須

11.2 Amazon Cognito オーソライザー

ユーザープール + API Gateway を組み合わせたOpenID Connect方式。

# API Gateway オーソライザー設定
Authorizers:
  - Type: COGNITO_USER_POOLS
    ProviderArn: arn:aws:cognito-idp:ap-northeast-1:123456789012:userpool/ap-northeast-1_aBcDeFgHi
    IdentitySource: method.request.header.Authorization
    AuthorizerResultTtlInSeconds: 300

フロントエンド側(SPA):

// Amazon Cognito SDK
const userPool = new AmazonCognitoIdentityServiceProvider.CognitoUserPool({
    UserPoolId: 'ap-northeast-1_aBcDeFgHi',
    ClientId: 'abc123def456'
});

const user = userPool.getCurrentUser();
user.getSession((err, session) => {
    const idToken = session.getIdToken().getJwtToken();
    
    fetch('https://api.example.com/users', {
        headers: {
            'Authorization': idToken
        }
    });
});

API Gateway は JWT を自動的に検証する(署名、有効期限)。

メリット:

  • ユーザー管理がビルトイン
  • JWT トークン自動検証
  • MFA, ソーシャルログイン対応

デメリット:

  • Lambda オーソライザーより複雑なカスタマイズ不可
  • ユーザープール利用料(月 $0.5 /100アクティブユーザー)

11.3 Lambda オーソライザー(最も柔軟)

カスタムロジックで認証を実装。3つのタイプ:

TOKEN 型(推奨):

def lambda_handler(event, context):
    """
    event['authorizationToken'] = 'Bearer <token>'
    event['methodArn'] = 'arn:aws:execute-api:ap-northeast-1:123456789012:abcdef123/prod/GET/users'
    """
    
    token = event['authorizationToken'].split(' ')[1]
    
    try:
        # カスタム認証ロジック
        decoded = verify_token(token)  # JWT検証、または外部API呼び出し
        principal_id = decoded['sub']
        
        policy = build_policy('Allow', event['methodArn'])
        policy['context'] = {
            'userId': principal_id,
            'email': decoded['email'],
            'role': decoded['role']
        }
        return policy
        
    except Exception as e:
        raise Exception('Unauthorized')

def build_policy(effect, resource):
    return {
        'principalId': 'user',  # APIアクセス元の識別子
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': 'execute-api:Invoke',
                    'Effect': effect,
                    'Resource': resource
                }
            ]
        }
    }

REQUEST 型(ヘッダー・クエリパラメータの詳細検査):

def lambda_handler(event, context):
    """
    request形式でくる
    - event['headers']
    - event['queryStringParameters']
    - event['requestContext']
    """
    
    auth_header = event['headers'].get('Authorization', '')
    
    # IP制限を含める
    source_ip = event['requestContext']['identity']['sourceIp']
    if source_ip not in WHITELIST_IPS:
        return deny_policy()
    
    return allow_policy()

キャッシング:

policy = {
    'principalId': 'user-123',
    'policyDocument': {...},
    'context': {
        'userId': 'user-123'
    },
    'usageIdentifierKey': 'api-key-123'  # キャッシュキー
}

# このキャッシュキーが同じ場合、5分間このポリシーが再利用される

メリット:

  • 完全なカスタマイズ(多要素認証、外部API連携など)
  • WebSocket認証対応

デメリット:

  • Lambda実行時間が認証遅延に
  • オーソライザー自体のエラー処理が複雑

比較表

方式認証スピードカスタマイズ用途
IAM高速AWS 内部API
Cognitoモバイル・SPA、ユーザー管理が必要
Lambda遅い(+50-200ms)カスタム認証ロジック、レガシーシステム連携

12. リソースポリシー — IP制限、VPC エンドポイント制限

IP ベースのアクセス制限

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": [
            "203.0.113.0/24",  // 許可するIP
            "198.51.100.0/24"
          ]
        },
        "Bool": {
          "aws:SecureTransport": "false"  // HTTPS強制
        }
      }
    }
  ]
}

VPC エンドポイント制限(プライベート API)

内部ユーザーのみがアクセス可能なAPI(インターネット非公開):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*",
      "Condition": {
        "StringNotEquals": {
          "aws:SourceVpc": "vpc-12345678"
        }
      }
    }
  ]
}

API Gateway の設定:

aws apigateway update-rest-api \
  --rest-api-id abcdef123 \
  --patch-operations op=replace,path=/endpointConfiguration/types/0,value=PRIVATE

これにより、API はVPCエンドポイント経由のみアクセス可能になる。


13. WAF 連携(REST API のみ)

HTTP API では WAF 非対応

API タイプWAF理由
REST APIサポートCloudFront との統合
HTTP API非サポート設計が簡潔(オーバーヘッド最小化)

WAF ルール設定

import boto3

waf = boto3.client('wafv2')

# Web ACL 作成
response = waf.create_web_acl(
    Name='api-protection',
    Scope='REGIONAL',
    DefaultAction={
        'Allow': {}
    },
    Rules=[
        {
            'Name': 'SQLiProtection',
            'Priority': 0,
            'Statement': {
                'SqliMatchStatement': {
                    'FieldToMatch': {
                        'Body': {}
                    },
                    'TextTransformations': [
                        {'Priority': 0, 'Type': 'URL_DECODE'},
                        {'Priority': 1, 'Type': 'HTML_ENTITY_DECODE'}
                    ]
                }
            },
            'Action': {
                'Block': {
                    'CustomResponse': {
                        'ResponseCode': 403,
                        'CustomResponseBodyKey': 'blocked'
                    }
                }
            },
            'VisibilityConfig': {
                'SampledRequestsEnabled': True,
                'CloudWatchMetricsEnabled': True,
                'MetricName': 'SQLiProtectionMetrics'
            }
        }
    ],
    VisibilityConfig={
        'SampledRequestsEnabled': True,
        'CloudWatchMetricsEnabled': True,
        'MetricName': 'api-protection-metrics'
    }
)

# REST API に関連付け
apigateway = boto3.client('apigateway')
apigateway.associate_web_acl(
    WebAclArn=response['Summary']['Arn'],
    ResourceArn=f'arn:aws:apigateway:ap-northeast-1::/restapis/abcdef123/stages/prod'
)

14. 使用量プランと API キー — スロットリングとクォータ

API キーはあくまで「ロードバランス」、認証ではない

# 危険な勘違い
# "API Key があれば誰でも呼べる" と考えるのは間違い

# 正しい理解
# API Key は複数のクライアントのレート制限を分離するだけ

使用量プランの設定

# 使用量プラン作成
aws apigateway create-usage-plan \
  --name enterprise-plan \
  --api-stages apiId=abcdef123,stage=prod \
  --throttle burstLimit=5000,rateLimit=2000 \
  --quota limit=1000000,period=DAY

# APIキー作成
key=$(aws apigateway create-api-key \
  --name client-api-key \
  --enabled \
  --query 'id' \
  --output text)

# 使用量プランにAPIキーをリンク
aws apigateway create-usage-plan-key \
  --usage-plan-id <usage-plan-id> \
  --key-id $key \
  --key-type API_KEY

スロットリング戦略

シナリオスロットル使用量プラン目的
パブリックAPI低(100 req/s)基本, プロフェッショナルDDoS対策
内部API高(10,000 req/s)エンタープライズバースト許容
リソース集約的非常に低(10 req/s)スターターリソース保護

15. mTLS(相互TLS認証)— 両者が証明書で認証

mTLS の必要性

通常のTLS(HTTPS):

  • サーバー証明書をクライアントが検証(サーバーの真正性確保)
  • クライアント側は認証なし

mTLS:

  • クライアント証明書 をサーバーが検証
  • B2B API で必須(金融機関、医療機関など)

API Gateway での mTLS 設定

# クライアント証明書を作成(自己署名例)
openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365 -nodes

# API Gateway で trust store を作成
aws apigateway create-domain-name \
  --domain-name api.example.com \
  --certificate-arn arn:aws:acm:... \
  --mutual-tls-authentication \
  --truststore-uri s3://my-bucket/truststore.pem \
  --truststore-version 1

truststore.pem(許可するクライアント証明書のCA):

-----BEGIN CERTIFICATE-----
MIIC...(クライアントCAの証明書)...
-----END CERTIFICATE-----

クライアント側の実装

import requests
from requests.auth import HTTPCertAuth

response = requests.get(
    'https://api.example.com/secure',
    cert=('client-cert.pem', 'client-key.pem'),
    verify='ca-bundle.crt'  # サーバーCA検証
)

16. プライベート API — VPC エンドポイント経由のみ

前述の「VPC エンドポイント制限」を実装したAPI。

アーキテクチャ

┌─────────────┐
│ VPC内の      │
│ アプリケーション├──┐
└─────────────┘  │

            ┌───▼───┐
            │VPC EP │
            │(API)  │
            └───┬───┘

        ┌───────▼─────────┐
        │ API Gateway     │
        │ (PRIVATE type)  │
        └───────┬─────────┘

        ┌───────▼─────────┐
        │  Lambda         │
        │ (VPC接続)       │
        └─────────────────┘

リソースポリシー

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*",
      "Condition": {
        "StringEquals": {
          "aws:sourceVpce": "vpce-0123456789abcdef0"
        }
      }
    },
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "execute-api:/*",
      "Condition": {
        "StringNotEquals": {
          "aws:sourceVpce": "vpce-0123456789abcdef0"
        }
      }
    }
  ]
}

17. CloudWatch ログ — 実行ログ vs アクセスログ

実行ログ(Execution Logs)

API Gateway が各リクエスト・レスポンスの詳細をログ出力。デバッグ向け、本番では無効推奨

{
  "extendedRequestId": "abcd1234==",
  "requestTime": "27/Apr/2026:10:00:00 +0000",
  "httpMethod": "POST",
  "resourcePath": "/users",
  "status": 200,
  "protocol": "HTTP/1.1",
  "responseLength": 256,
  "error": null,
  "integrationErrorMessage": null,
  "authorizerError": null,
  "authorizerIntegrationStatus": 200,
  "authorizer": {
    "principalId": "user-123",
    "integrationLatency": 45
  },
  "integration": {
    "requestTemplateSelectionExpression": "$default",
    "httpMethod": "POST",
    "type": "AWS_PROXY",
    "status": 200,
    "latency": 120,
    "responseParameters": {}
  },
  "domainName": "api.example.com",
  "domainPrefix": "api",
  "stage": "prod",
  "connectedTime": 1682000400000,
  "ttl": 3600
}

アクセスログ(Access Logs)

本番環境で必須。リクエスト概要のみ(実行ログより軽量)。

# アクセスログフォーマット定義
$context.requestId
$context.extendedRequestId
$context.identity.sourceIp
$context.identity.userAgent
$context.requestTime
$context.httpMethod
$context.resourcePath
$context.status
$context.protocol
$context.responseLength
$context.error
$context.authorizer.principalId

CloudWatch Logs IAM ロール:

{
  "Effect": "Allow",
  "Action": [
    "logs:CreateLogDelivery",
    "logs:GetLogDelivery",
    "logs:UpdateLogDelivery",
    "logs:DeleteLogDelivery",
    "logs:ListLogDeliveries",
    "logs:PutResourcePolicy",
    "logs:DescribeResourcePolicies",
    "logs:DescribeLogGroups"
  ],
  "Resource": "*"
}

18. CORS 設定のセキュリティ — ワイルドカード回避

危険な設定

# 全てのオリジンを許可(セキュリティ上NG)
CorsConfiguration:
  AllowedOrigins:
    - "*"
  AllowedMethods:
    - GET
    - POST
    - PUT
    - DELETE
  AllowedHeaders:
    - "*"
  MaxAge: 86400

安全な設定

CorsConfiguration:
  AllowedOrigins:
    - "https://app.example.com"      # 本番環境
    - "https://staging.example.com"  # ステージング
    - "http://localhost:3000"        # 開発環境(HTTPOKの根拠あり)
  AllowedMethods:
    - GET
    - POST
  AllowedHeaders:
    - Content-Type
    - Authorization
    - X-Amz-Date
  ExposedHeaders:
    - X-Request-Id
  MaxAge: 3600  # 1時間(短め)
  AllowCredentials: true  # Cookie送信許可

OPTIONS プリフライトリクエスト

// ブラウザが自動的に送信(開発者が意識する必要はない)
// CORS対応しないAPIはここで失敗

// リクエスト
OPTIONS /users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

// レスポンス
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 3600

19. REST API vs HTTP API — セキュリティ機能の差異

機能REST APIHTTP API備考
IAM認証リソースベースポリシーも同じ
Cognito認証
Lambda オーソライザー
mTLS✅(v2.0のみ)
WAF連携REST限定
リソースベースポリシー
VPCエンドポイント
実行ログ/アクセスログ

選定基準:

  • REST API: エンタープライズ、複雑な認可、WAF必須
  • HTTP API: スタートアップ、シンプルなAPI、コスト最適化重視

20. API Gateway の制限 — できないこと

APIキーは認証ではない

APIキーは以下を提供しない

  • ユーザーの身元確認
  • リクエストの完全性検証
  • 暗号化
# 危険な実装
api_key = request.headers.get('x-api-key')
if api_key == 'hardcoded-secret':
    return allow_request()

# 正しい実装
api_key = request.headers.get('x-api-key')
key_metadata = validate_api_key_in_database(api_key)  # DBから検証
if key_metadata['is_active'] and key_metadata['expires_at'] > now():
    return allow_request()

WAF は REST API のみ

HTTP API を使用している場合、WAF 保護はCloudFront + Lambda@Edge で実装:

# CloudFront + Lambda@Edge でのWAF代替
def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']
    
    # SQL injection 検出
    if 'sql' in request['querystring'].lower():
        return {
            'status': 403,
            'statusDescription': 'Forbidden'
        }
    
    return request

第3部: 連携パターン

21. パターン 1: API Gateway + Lambda + Cognito

フロー図

User

[SPA (React)]
  ↓ (1) login: username/password
[Cognito Auth]
  ↓ (2) returns: ID Token, Access Token
[SPA stores tokens]
  ↓ (3) API request with ID Token
[API Gateway - Cognito Authorizer]
  ↓ (4) JWT 検証, ユーザーID抽出
[Lambda Function]
  ↓ (5) DynamoDB or RDS
[Database]

コード例

フロント(React + Amplify):

import { Auth } from 'aws-amplify';

async function loginUser(username: string, password: string) {
    try {
        const user = await Auth.signIn(username, password);
        const session = await Auth.currentSession();
        const idToken = session.idToken.jwtToken;
        
        // API呼び出し
        const response = await fetch('https://api.example.com/profile', {
            headers: {
                'Authorization': idToken
            }
        });
    } catch (error) {
        console.error('Login failed:', error);
    }
}

Lambda実行ロール:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/users"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/*"
    }
  ]
}

22. パターン 2: API Gateway + VPCリンク + プライベートバックエンド

アーキテクチャ(オンプレミス統合)

API Gateway + VPCリンク構成

Terraform実装例

# VPCリンク作成
resource "aws_apigateway_vpc_link" "private_backend" {
  name            = "private-backend-link"
  description     = "VPC Link to Private NLB"
  target_arns     = [aws_lb.private_nlb.arn]
  tags = {
    Environment = "production"
  }
}

# API Gateway統合
resource "aws_apigateway_integration" "backend" {
  rest_api_id      = aws_apigateway_rest_api.example.id
  resource_id      = aws_apigateway_resource.proxy.id
  http_method      = "ANY"
  type             = "HTTP_PROXY"
  integration_http_method = "POST"
  uri              = "https://${aws_lb.private_nlb.dns_name}"
  
  vpc_link_id      = aws_apigateway_vpc_link.private_backend.id
  
  request_parameters = {
    "integration.request.header.Authorization" = "method.request.header.Authorization"
  }
}

# NLB(プライベート)
resource "aws_lb" "private_nlb" {
  name               = "private-backend-nlb"
  internal           = true
  load_balancer_type = "network"
  subnets            = [aws_subnet.private_a.id, aws_subnet.private_b.id]
  
  enable_deletion_protection = true
  
  tags = {
    Name = "private-backend-nlb"
  }
}

# NLB リスナー(TLS)
resource "aws_lb_listener" "tls" {
  load_balancer_arn = aws_lb.private_nlb.arn
  port              = 443
  protocol          = "TLS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = aws_acm_certificate.nlb.arn
  
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.backend.arn
  }
}

# セキュリティグループ(NLBインバウンド)
resource "aws_security_group_rule" "nlb_from_apigw" {
  type                     = "ingress"
  from_port                = 443
  to_port                  = 443
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.api_gateway.id
  security_group_id        = aws_security_group.nlb.id
}

23. パターン 3: Lambda + Secrets Manager + RDS Proxy

RDS Proxy の役割

RDSへの接続プール管理。Lambda が毎回新規接続を作成する代わりに、Proxyが接続を再利用。

Lambda 1 ──┐
Lambda 2 ──┼─→ RDS Proxy ──→ RDS (1-5 接続)
Lambda 3 ──┘
           (数百 接続)

利点:
- "Too Many Connections" エラー回避
- レイテンシー削減(接続確立不要)
- 接続キープアライブ(アイドル切断対策)

Secrets Manager 統合

import boto3
import pymysql
from botocore.exceptions import ClientError

secrets_client = boto3.client('secretsmanager')

def get_db_connection():
    try:
        # Secrets Manager から認証情報取得
        secret_response = secrets_client.get_secret_value(
            SecretId='prod/rds/mysql'
        )
        secret = json.loads(secret_response['SecretString'])
        
        # RDS Proxy 経由で接続(Secrets Manager 認証)
        connection = pymysql.connect(
            host=f"{secret['proxy_endpoint']}:3306",  # RDS Proxy endpoint
            user=secret['username'],
            password=secret['password'],
            database=secret['dbname'],
            ssl_verify_cert=True,
            ssl_verify_identity=True  # mTLS
        )
        return connection
        
    except ClientError as e:
        print(f"Failed to retrieve secret: {e}")
        raise

def lambda_handler(event, context):
    conn = None
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users LIMIT 1")
        result = cursor.fetchone()
        return {
            'statusCode': 200,
            'body': json.dumps({'user': result})
        }
    finally:
        if conn:
            conn.close()

RDS Proxy IAM 認証

# パスワード認証ではなくIAM認証を使用(セキュリティ向上)
import boto3
from botocore.exceptions import ClientError

rds = boto3.client('rds')

def get_db_token():
    """RDS Proxy IAM認証トークン取得(15分有効)"""
    token = rds.generate_db_auth_token(
        DBHostname='proxy.123456789012.ap-northeast-1.rds.amazonaws.com',
        Port=3306,
        DBUser='iam_user',  # DB内で CREATE USER 'iam_user'@'%' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'
        Region='ap-northeast-1'
    )
    return token

# パスワード代わりにトークンを使用
connection = pymysql.connect(
    host='proxy.123456789012.ap-northeast-1.rds.amazonaws.com',
    user='iam_user',
    password=get_db_token(),
    database='mydb',
    ssl={'ssl': True}
)

Lambda 実行ロール:

{
  "Effect": "Allow",
  "Action": [
    "rds-db:connect"
  ],
  "Resource": "arn:aws:rds:ap-northeast-1:123456789012:db:my-database/iam_user"
}

試験で狙われるポイント(最終まとめ)

Lambda

  1. 実行ロール vs リソースベースポリシー

    • 実行ロール: Lambda「ができること」
    • リソースベースポリシー: 「誰がLambdaを呼べるか」
    • クロスアカウント = リソースベースポリシー
  2. VPC接続 Lambda

    • NAT Gateway必須(インターネット接続時)
    • VPC エンドポイント で NAT コスト削減
    • セキュリティグループ設定忘れず
  3. 環境変数暗号化

    • デフォルト = AWS管理キー(無料、制御不可)
    • カスタムKMS = ユーザー管理(有料、制御可)
    • 機密情報は Secrets Manager 推奨
  4. 予約済み同時実行数

    • DDoS対策の一部
    • Lambda全体ではなく関数ごと設定
    • ThrottlingException で拒否
  5. 実行時間15分上限

    • 長時間処理はStep Functions
    • バッチはSQS+ポーリング

API Gateway

  1. 認証方式3つ

    • IAM: 内部API
    • Cognito: ユーザー管理必須
    • Lambda: カスタムロジック(遅い)
  2. リソースポリシー

    • IP制限、VPC制限、クロスアカウント
    • ワイルドカード避ける
  3. WAF は REST API のみ

    • HTTP API は CloudFront + Lambda@Edge
  4. APIキーは認証ではない

    • スロットリング・クォータのみ
    • 別途認証が必須
  5. mTLS(相互TLS)

    • クライアント証明書でサーバーが検証
    • B2B API で必須
  6. プライベートAPI

    • VPCエンドポイント経由のみ
    • リソースポリシーで VPC制限
  7. CORS設定

    • ワイルドカード許可は NG
    • AllowedOrigins を明示的に指定
    • MaxAge は短め(3600秒程度)
  8. ログ

    • 実行ログ: デバッグ向け(本番で無効)
    • アクセスログ: 本番必須

参考資料


次章予告: EC2・ECS・EKS のセキュリティ(IAM ロール、セキュリティグループ、IMDS、Pod Security Policy)