【AWSハンズオン】第10回 CORS設定とセキュリティ強化

【AWSハンズオン】第10回 CORS設定とセキュリティ強化

前回構築したフロントエンド(CloudFront + S3)とバックエンド(ALB + ECS)間の通信を安全に行うため、CORS設定とセキュリティ強化を実装していきましょう。

AWS #API#AWS#ハンズオン

【AWSハンズオン】第10回 CORS設定とセキュリティ強化

サムネイル

前回構築したフロントエンド(CloudFront + S3)とバックエンド(ALB + ECS)間の通信を安全に行うため、CORS設定とセキュリティ強化を実装していきましょう。

更新日: 8/11/2025

今回作業対象のブランチ

前提条件

  • 第1回から第9回までの構築が完了していること
  • フロントエンドとバックエンドが別ドメインで動作していること
  • こちらからDockerコンテナでの実行環境が準備できていること

今回使用するHonoのサンプルコードはこちらから

Terraform実行環境の起動

前回と同様に、Dockerコンテナ内で作業を進めます。

# コンテナを起動してbashに入る
docker-compose run --rm terraform

これ以降のコマンドは、すべてこのコンテナ内で実行します。

基礎知識

CORS(Cross-Origin Resource Sharing)とは

CORSは、異なるオリジン間でのリソース共有を安全に行うための仕組みです。

WEBアプリケーション開発者の視点から見ると

ローカル開発では、フロントエンド(localhost:3000)からバックエンド(localhost:8080)にAPIリクエストを送ると、ブラウザがCORSエラーを出すことがあります。これは、ブラウザが異なるオリジン間の通信を制限しているためです。本番環境でも、CloudFrontドメインからALBドメインへのアクセスで同様の問題が発生します。

オリジンとは

構成要素 説明
プロトコル https http/httpsの違いでも別オリジン
ドメイン example.com サブドメインも含めて完全一致が必要
ポート 443 デフォルトポート以外は明示的に指定

CSP(Content Security Policy)とは

CSPは、XSS攻撃などを防ぐためのセキュリティレイヤーです。どのリソースの読み込みを許可するかをブラウザに指示します。

CSPディレクティブの例

ディレクティブ 用途 設定例
default-src デフォルトのソース制限 ‘self’
script-src JavaScriptの読み込み元 ‘self’ ‘unsafe-inline’
style-src CSSの読み込み元 ‘self’ https://fonts.googleapis.com
img-src 画像の読み込み元 ‘self’ data: https:
connect-src APIやWebSocketの接続先 ‘self’ https://api.example.com

セキュリティ脅威と対策

脅威 説明 対策
XSS 悪意のあるスクリプトの実行 CSP、入力値検証、出力エスケープ
CSRF ユーザーの意図しない操作の実行 CSRFトークン、SameSite Cookie
クリックジャッキング 透明なiframeでの誘導 X-Frame-Options、CSP frame-ancestors

今回作成するネットワーク構成

リソース 説明 数量
ALB Listener Rule CORS対応のためのカスタムルール 1個
Response Headers セキュリティヘッダーの設定 複数
WAF WebACL 基本的な攻撃からの保護 1個
WAF Rules SQLインジェクション、XSS対策 複数
API認証用リソース JWTトークン検証用の設定 1セット

Terraformコードの実装

変数の追加

CORS設定とセキュリティ関連の変数を追加します。

# terraform/variables.tf に追加

variable "allowed_origins" {
  description = "CORS許可するオリジンのリスト"
  type        = list(string)
  default     = []  # 実際の値はterraform.tfvarsで設定
}

variable "enable_waf" {
  description = "WAFを有効にするかどうか"
  type        = bool
  default     = true
}

variable "api_rate_limit" {
  description = "APIレート制限(5分間のリクエスト数)"
  type        = number
  default     = 2000
}

variable "enable_security_headers" {
  description = "セキュリティヘッダーを有効にするかどうか"
  type        = bool
  default     = true
}

ALB Listener Rule の更新

terraform/alb.tf から既存のHTTP Listener既存のListenerを削除して、CORS対応とセキュリティヘッダーを追加します。

# terraform/alb_cors.tf

# 既存のListenerを削除して、新しいListener Ruleを作成
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "fixed-response"
    fixed_response {
      content_type = "text/plain"
      message_body = "Not Found"
      status_code  = "404"
    }
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-http-listener"
  }
}

# APIエンドポイント用のListener Rule
resource "aws_lb_listener_rule" "api" {
  listener_arn = aws_lb_listener.http.arn
  priority     = 100

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ecs.arn
  }

  condition {
    path_pattern {
      values = ["/api/*", "/health"]
    }
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-api-rule"
  }
}

# CORS Preflight用のListener Rule
resource "aws_lb_listener_rule" "cors_preflight" {
  listener_arn = aws_lb_listener.http.arn
  priority     = 99

  action {
    type = "fixed-response"
    fixed_response {
      content_type = "text/plain"
      message_body = ""
      status_code  = "200"
    }
  }

  condition {
    http_request_method {
      values = ["OPTIONS"]
    }
  }

  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-cors-preflight"
  }
}

ALB Listener Rule設定の説明

パラメータ 設定値 設定理由
priority 99(preflight), 100(API) 数値が小さいほど優先度が高い。OPTIONSメソッドを先に評価
path_pattern /api/*, /health APIエンドポイントとヘルスチェックパスを指定
http_request_method OPTIONS プリフライトリクエストの判定
status_code 200 プリフライトリクエストには200を返す

WAFの設定

基本的なWeb攻撃から保護するWAFを設定します。

# terraform/waf.tf

# WAF WebACL
resource "aws_wafv2_web_acl" "main" {
  count = var.enable_waf ? 1 : 0

  name  = "${var.project_name}-${var.environment}-waf"
  scope = "REGIONAL"

  default_action {
    allow {}
  }

  # レート制限ルール
  rule {
    name     = "RateLimitRule"
    priority = 1

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = var.api_rate_limit
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "RateLimitRule"
      sampled_requests_enabled   = true
    }
  }

  # AWS Managed Rules - Core Rule Set
  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 2

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "CommonRuleSet"
      sampled_requests_enabled   = true
    }
  }

  # AWS Managed Rules - Known Bad Inputs
  rule {
    name     = "AWSManagedRulesKnownBadInputsRuleSet"
    priority = 3

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesKnownBadInputsRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "KnownBadInputs"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "${var.project_name}-${var.environment}-waf"
    sampled_requests_enabled   = true
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-waf"
  }
}

# WAFとALBの関連付け
resource "aws_wafv2_web_acl_association" "alb" {
  count = var.enable_waf ? 1 : 0

  resource_arn = aws_lb.main.arn
  web_acl_arn  = aws_wafv2_web_acl.main[0].arn
}

WAF設定の説明

パラメータ 設定値 設定理由
scope REGIONAL ALBに適用するためREGIONALを指定(CloudFrontの場合はCLOUDFRONT)
default_action allow デフォルトは許可し、ルールに該当する場合のみブロック
rate_based_statement.limit 2000 5分間で2000リクエストを超えるとブロック
aggregate_key_type IP IPアドレス単位でレート制限を適用
AWSManagedRulesCommonRuleSet 有効 SQLインジェクション、XSS等の一般的な攻撃を防御
AWSManagedRulesKnownBadInputsRuleSet 有効 既知の悪意ある入力パターンをブロック

HonoでのCORS実

サンプルコードとして実装済みコードを例にして、CORSの設定例を挙げていきます。

// backend/src/middleware/cors.ts

import { cors } from 'hono/cors'
import type { MiddlewareHandler } from 'hono'

export const corsMiddleware = (): MiddlewareHandler => {
  const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || []
  
  return cors({
    origin: (origin) => {
      // 開発環境では全てのオリジンを許可
      if (process.env.NODE_ENV === 'development') {
        return origin
      }
      
      // 本番環境では許可リストのオリジンのみ
      if (allowedOrigins.includes(origin)) {
        return origin
      }
      
      return null
    },
    allowHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
    allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    exposeHeaders: ['X-Total-Count'],
    maxAge: 86400, // 24時間
    credentials: true,
  })
}

セキュリティヘッダーの実装

こちらもサンプルコードとして実装済みコードを例にして、セキュリティヘッダーの実装例を挙げていきます。

// backend/src/middleware/security.ts

import type { MiddlewareHandler } from 'hono'

export const securityHeaders = (): MiddlewareHandler => {
  return async (c, next) => {
    await next()
    
    // セキュリティヘッダーの設定
    c.header('X-Content-Type-Options', 'nosniff')
    c.header('X-Frame-Options', 'DENY')
    c.header('X-XSS-Protection', '1; mode=block')
    c.header('Referrer-Policy', 'strict-origin-when-cross-origin')
    c.header('Permissions-Policy', 'geolocation=(), microphone=(), camera=()')
    
    // CSPヘッダー
    const cspDirectives = [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self'",
      "connect-src 'self' " + (process.env.ALLOWED_ORIGINS || ''),
      "frame-ancestors 'none'",
    ].join('; ')
    
    c.header('Content-Security-Policy', cspDirectives)
  }
}

CSRF対策の実装

こちらもサンプルコードとして実装済みコードを例にして、CSRF対策の実装例を挙げていきます。

// backend/src/middleware/csrf.ts

import { createMiddleware } from 'hono/factory'
import crypto from 'crypto'

export const csrf = () => {
  return createMiddleware(async (c, next) => {
    // GETリクエストはスキップ
    if (c.req.method === 'GET' || c.req.method === 'HEAD') {
      await next()
      return
    }
    
    const token = c.req.header('X-CSRF-Token')
    const sessionToken = c.get('csrfToken') // セッションから取得
    
    if (!token || token !== sessionToken) {
      return c.json({ error: 'Invalid CSRF token' }, 403)
    }
    
    await next()
  })
}

// CSRFトークン生成エンドポイント
export const generateCSRFToken = () => {
  const token = crypto.randomBytes(32).toString('hex')
  return token
}

Task Definitionの環境変数追加

ECSタスクにCORS設定用の環境変数を追加します。

# terraform/ecs_service.tf の container_definitions 内の environment に追加

environment = [
  {
    name  = "NODE_ENV"
    value = "production"
  },
  {
    name  = "PORT"
    value = "3000"
  },
  {
    name  = "ALLOWED_ORIGINS"
    value = join(",", concat(
      ["https://${aws_cloudfront_distribution.frontend.domain_name}"],
      var.allowed_origins
    ))
  },
  {
    name  = "ENABLE_SECURITY_HEADERS"
    value = var.enable_security_headers ? "true" : "false"
  }
]

ECS Serviceの依存関係修正

ALB Listener Ruleの変更に伴い、ECS Serviceの依存関係も更新する必要があります。

# terraform/ecs_service.tf のdepends_onを修正

depends_on = [
  aws_lb_listener.http,
  aws_lb_listener_rule.api,  # 追加:API用のListener Ruleへの依存関係
  aws_iam_role_policy.ecs_task_role_policy
]

依存関係を追加する理由

修正前 修正後 理由
aws_lb_listener.httpのみ aws_lb_listener_rule.apiも追加 Target GroupがListener Ruleと関連付けられてから、ECS Serviceが起動する必要がある

ECS Serviceが起動する前に、ALBのListener RuleでTarget Groupが関連付けられることになります。

出力の追加

作成したリソースの情報を出力に追加します。

# terraform/outputs.tf に追加

# WAF関連の出力
output "waf_web_acl_id" {
  description = "WAF WebACL ID"
  value       = try(aws_wafv2_web_acl.main[0].id, "")
}

output "waf_web_acl_arn" {
  description = "WAF WebACL ARN"
  value       = try(aws_wafv2_web_acl.main[0].arn, "")
}

# セキュリティ設定の確認用
output "cors_allowed_origins" {
  description = "CORS許可されているオリジン"
  value = concat(
    ["https://${aws_cloudfront_distribution.frontend.domain_name}"],
    var.allowed_origins
  )
}

terraform.tfvarsの設定

CORS許可するオリジンを設定します。

# terraform/terraform.tfvars に追加

# CloudFrontのドメインは自動的に追加されるため、追加のオリジンのみ指定
allowed_origins = [
  # "https://example.com"  # カスタムドメインを使用する場合
]

# WAFを有効化
enable_waf = true

# APIレート制限(5分間)
api_rate_limit = 2000

# セキュリティヘッダーを有効化
enable_security_headers = true

リソースを作成

Terraformコンテナ内で以下のコマンドを実行します。

# terraformディレクトリへ移動
cd terraform

# 実行計画の確認
terraform plan

# リソースの作成
terraform apply

WAFの作成には2〜3分程度かかります。

作成結果の確認

# WAF WebACL IDの確認
terraform output waf_web_acl_id

# CORS許可オリジンの確認
terraform output cors_allowed_origins

動作確認

CORSの動作確認

フロントエンドからAPIへのリクエストが正しく動作することを確認します。

// ブラウザのコンソールで実行
const albDns = 'ALBのDNS名';
const response = await fetch(`http://${albDns}/api/users`, {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
  },
  credentials: 'include'
});

// CORSヘッダーの確認
console.log('Access-Control-Allow-Origin:', response.headers.get('Access-Control-Allow-Origin'));

セキュリティヘッダーの確認

# セキュリティヘッダーの確認
ALB_DNS=$(terraform output -raw alb_dns_name)
curl -I http://$ALB_DNS/api/users

レスポンスヘッダーの期待値

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; ...

WAFの動作確認

レート制限が正しく動作することを確認します。

# レート制限のテスト(2000リクエスト以上送信)
for i in {1..2100}; do
  curl -s -o /dev/null -w "%{http_code}\n" http://$ALB_DNS/health &
done
wait

2000リクエストを超えると403エラーが返されることを確認します。

トラブルシューティング

CORSエラーが発生する場合

ブラウザのコンソールでエラーメッセージを確認

Access to fetch at 'http://alb-dns/api/users' from origin 'https://cloudfront-dns' has been blocked by CORS policy

対処法

確認項目 対処方法
Allowed Originsの設定 terraform outputで許可オリジンを確認し、正しく設定されているか確認
プリフライトリクエスト OPTIONSメソッドが200を返しているか確認
レスポンスヘッダー Access-Control-Allow-Originヘッダーが含まれているか確認

WAFでブロックされる場合

AWS Management ConsoleでWAFのログを確認し、どのルールでブロックされているか特定します。

# WAFのサンプルリクエストを確認
aws wafv2 get-sampled-requests \
  --web-acl-arn $(terraform output -raw waf_web_acl_arn) \
  --rule-metric-name RateLimitRule \
  --scope REGIONAL \
  --time-window StartTime=$(date -u -d '5 minutes ago' +%s),EndTime=$(date +%s) \
  --max-items 10

コスト管理

サービス 項目 料金
WAF WebACL $5.00/月
ルール $1.00/月・ルール
リクエスト $0.60/100万リクエスト
ALB ルール評価 $0.008/時間・ルール

開発環境では、WAFを無効化することでコストを削減できます。

# terraform.tfvars
enable_waf = false  # 開発時は無効化

次のステップ

次回は、監視とログ設定を行います。

  • CloudWatch Container Insightsの詳細設定
  • VPC Flow Logsの分析
  • ALB Access Logsの活用
  • AWS X-Rayによる分散トレーシング
  • アプリケーションパフォーマンスの可視化

検索

検索条件に一致する記事が見つかりませんでした