前提条件
- 第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による分散トレーシング
- アプリケーションパフォーマンスの可視化