セキュリティの基礎 — システムを守る
攻撃が来た日
EchoTaskが有名になると、攻撃者も増えた。
# WAFのログ(1時間分)
$ aws wafv2 get-sampled-requests ... | jq '.SampledRequests[] | .Request.URI'
"/api/v1/users?id=1 OR 1=1--" ← SQLインジェクション試み
"/api/v1/tasks?search=<script>" ← XSS試み
"/api/v1/admin" ← 管理画面スキャン
"/../../../etc/passwd" ← パストラバーサル試み「セキュリティを後回しにしてきたツケだ」——ハルトは画面を見つめながら反省した。 攻撃の種類はOWASP Top 10に分類されるものばかりだった。 放置すれば、ユーザーデータが漏洩する。信頼は一瞬で崩れる。 ハルトは多層防御の設計に取り掛かった。
多層防御(Defense in Depth)
セキュリティは1つの壁ではなく、複数の層で守る。
1層が突破されても、次の層が守る。攻撃者のコストを最大化することが目的だ。
OWASP Top 10 — 主要な脆弱性とRailsでの対策
OWASP(Open Web Application Security Project)が毎年公開する脆弱性ランキングは、 セキュリティ設計の出発点として業界標準になっている。
A01: アクセス制御の不備
認可チェック漏れ。「他人のタスクが見れてしまう」パターン。
# 危険: ユーザーIDを検証せずに直接アクセス
# GET /api/v1/tasks/999 ← 他人のタスクIDを推測して叩ける
def show
@task = Task.find(params[:id]) # ← 誰でも取得できる
render json: @task
end
# 安全: current_userのスコープに限定する
def show
@task = current_user.tasks.find(params[:id])
# → 他人のタスクはRecordNotFoundになる
render json: @task
end管理者機能には before_action :require_admin! を必ず付ける。
module Admin
class BaseController < ApplicationController
before_action :require_admin!
private
def require_admin!
return if current_user&.admin?
render json: { error: 'Forbidden' }, status: :forbidden
end
end
endA03: インジェクション(SQLインジェクション)
# 危険: ユーザー入力を直接SQLに埋め込む
Task.where("title = '#{params[:title]}'")
# params[:title] = "'; DROP TABLE tasks;--" で全テーブル削除
# 安全: プレースホルダーを使う(Railsのデフォルト)
Task.where("title = ?", params[:title])
Task.where(title: params[:title])
# 複数パラメータのケース
Task.where(
"status = :status AND project_id = :project_id",
status: params[:status],
project_id: params[:project_id]
)WARNING
find_by_sql や execute を使うときは特に注意。
ORMを迂回する生SQL実行では、プレースホルダーを使い忘れやすい。
Brakemanの静的解析でも検出されるため、CI必須にしておく。
A07: 認証の失敗
セッション管理・パスワードの弱さ・ブルートフォースへの無防備が原因。 deviseのセキュアな設定で対策する。
# Gemfile
gem 'devise'
gem 'devise-two-factor'
# config/initializers/devise.rb
Devise.setup do |config|
# パスワードの最小文字数
config.password_length = 12..128
# アカウントロック(5回失敗で30分ロック)
config.lock_strategy = :failed_attempts
config.maximum_attempts = 5
config.unlock_strategy = :time
config.unlock_in = 30.minutes
# タイムアウト(2時間でセッション切れ)
config.timeout_in = 2.hours
# パスワードリセットリンクの有効期限
config.reset_password_within = 6.hours
# Eメール確認必須
config.reconfirmable = true
end# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:lockable, :timeoutable, :confirmable,
:two_factor_authenticatable
# パスワード強度チェック(正規表現)
validates :password,
format: {
with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])/,
message: '大文字・小文字・数字・記号を含む必要があります'
},
if: :password_required?
end認証と認可の違い
「あなたは誰か?」が認証(Authentication)。 「あなたに何が許可されているか?」が認可(Authorization)。
混同しがちだが、役割は完全に別物だ。
OAuth2・OIDCの認証フロー
EchoTaskはGoogle OIDCでのソーシャルログインを導入した。 トークン発行の流れをシーケンス図で整理する。
Amazon Cognito — マネージドユーザー管理
自前でユーザー管理基盤を作るより、Cognitoに任せる方が安全で速い。 MFA・パスワードポリシー・トークン管理が全部含まれている。
# CloudFormation: Cognitoユーザープール
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: echo-task-users
Policies:
PasswordPolicy:
MinimumLength: 12
RequireUppercase: true
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
MfaConfiguration: OPTIONAL
EnabledMfas: [SOFTWARE_TOKEN_MFA]
AutoVerifiedAttributes: [email]
AccountRecoverySetting:
RecoveryMechanisms:
- { Name: verified_email, Priority: 1 }
Schema:
- { Name: email, Required: true, Mutable: false }
- { Name: custom:role, AttributeDataType: String, Mutable: true }
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref CognitoUserPool
ClientName: echo-task-web
GenerateSecret: false
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
AccessTokenValidity: 1 # 1時間
IdTokenValidity: 1 # 1時間
RefreshTokenValidity: 30 # 30日
TokenValidityUnits:
AccessToken: hours
IdToken: hours
RefreshToken: days
CallbackURLs: [https://echotask.app/auth/callback]
AllowedOAuthFlows: [code]
AllowedOAuthScopes: [openid, email, profile]AWS KMS — カスタム暗号化
S3・RDSのデフォルト暗号化はAWS管理キー(SSE-S3)だが、 規制要件が厳しい場合は**Customer Managed Key(CMK)**を使う。
データキーの概念
KMSは「エンベロープ暗号化」を採用している。
- CMKでデータキーを生成する
- データキーで実際のデータを暗号化する
- 暗号化されたデータキーをデータと一緒に保存する
- 復号時はCMKで暗号化されたデータキーを復号し、それでデータを復号する
# CloudFormation: カスタムKMSキー
EchoTaskCMK:
Type: AWS::KMS::Key
Properties:
Description: EchoTask Customer Managed Key
EnableKeyRotation: true # 年次自動ローテーション
KeyPolicy:
Statement:
- Sid: EnableRootAccess
Effect: Allow
Principal: { AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" }
Action: kms:*
Resource: "*"
- Sid: AllowECSDecrypt
Effect: Allow
Principal: { AWS: !GetAtt ECSTaskRole.Arn }
Action: [kms:Decrypt, kms:GenerateDataKey]
Resource: "*"
# RDSにCMKを適用
RDSCluster:
Type: AWS::RDS::DBCluster
Properties:
StorageEncrypted: true
KmsKeyId: !Ref EchoTaskCMK# アプリレベルの暗号化(個人情報フィールド)
# Gemfile
gem 'lockbox' # フィールドレベル暗号化
gem 'blind_index' # 暗号化したまま検索可能
# config/initializers/lockbox.rb
Lockbox.master_key = Rails.application.credentials.lockbox_master_key
# app/models/user.rb
class User < ApplicationRecord
encrypts :phone_number, :address # DBには暗号化して保存
blind_index :phone_number # 暗号化したまま検索インデックス
end
# 使い方は通常のActiveRecordと同じ
user = User.find_by_phone_number!("+81-90-XXXX-XXXX")
puts user.phone_number # → 復号された値が返るAWS GuardDuty・SecurityHub — 脅威検知の自動化
人間が24時間監視するのは不可能だ。GuardDutyに任せる。
GuardDuty:
Type: AWS::GuardDuty::Detector
Properties:
Enable: true
FindingPublishingFrequency: FIFTEEN_MINUTES
DataSources:
S3Logs: { Enable: true }
MalwareProtection:
ScanEc2InstanceWithFindings: { EbsVolumes: true }
SecurityHub:
Type: AWS::SecurityHub::Hub
Properties:
AutoEnableControls: true
# GuardDutyの検知(High以上)をSlackに通知
GuardDutyEventRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source: [aws.guardduty]
detail-type: [GuardDuty Finding]
detail:
severity: [{ numeric: [>=, 7] }]
Targets:
- Arn: !Ref SlackNotificationLambdaArn
Id: SlackNotifyGuardDutyが検知する主な脅威:
| 脅威の種類 | 検知例 |
|---|---|
| UnauthorizedAccess | 異常な国からのAPIコール |
| Backdoor | C&Cサーバーへの通信 |
| CryptoCurrency | マイニングソフトウェアの実行 |
| Recon | ポートスキャン・brute force |
INFO
SecurityHubはCIS AWS Foundationsベンチマーク準拠チェックも自動化してくれる。 S3のパブリックアクセス設定・MFAの有無・CloudTrailの有効化など、 数百の項目を自動でスコアリングし、改善優先度を示してくれる。 月次のセキュリティレビューをSecurityHubダッシュボードから始めると効率がいい。
セキュリティヘッダー — RailsとGolangでの設定
ブラウザに「何を許可するか」を明示するHTTPヘッダーは、XSS・クリックジャッキングの防御になる。
Railsでのセキュリティヘッダー設定
# config/initializers/security_headers.rb
Rails.application.config.action_dispatch.default_headers = {
# クリックジャッキング防止(iframeに埋め込み禁止)
'X-Frame-Options' => 'DENY',
# MIMEスニッフィング防止
'X-Content-Type-Options' => 'nosniff',
# XSSフィルター(レガシーブラウザ向け)
'X-XSS-Protection' => '1; mode=block',
# HTTPS強制(1年間、サブドメイン含む)
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload',
# リファラーポリシー(外部サイトにURLを送らない)
'Referrer-Policy' => 'strict-origin-when-cross-origin',
# Content Security Policy(インラインスクリプト禁止)
'Content-Security-Policy' =>
"default-src 'self'; " \
"script-src 'self' 'nonce-#{SecureRandom.base64(16)}'; " \
"style-src 'self' 'unsafe-inline'; " \
"img-src 'self' data: https:; " \
"connect-src 'self' https://api.echotask.app; " \
"frame-ancestors 'none'"
}GolangでのセキュアなHTTPサーバー設定
Goでサービスを書く場合のセキュア設定(TLS 1.2以上・タイムアウト・セキュリティヘッダー)。
package main
import (
"crypto/tls"
"net/http"
"time"
)
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
server := &http.Server{
Addr: ":8443",
Handler: securityHeaders(mux),
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12, // TLS 1.2以上のみ
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
},
ReadTimeout: 10 * time.Second, // Slowloris攻撃対策
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
server.ListenAndServeTLS("cert.pem", "key.pem")
}AWS VPC — ネットワーク分離
VPC(Virtual Private Cloud)でプライベートネットワークを構築し、不要な外部アクセスを遮断する。
VPC:
Type: AWS::EC2::VPC
Properties: { CidrBlock: 10.0.0.0/16, EnableDnsHostnames: true }
PublicSubnet1: # ALB・NAT Gateway用
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: ap-northeast-1a
MapPublicIpOnLaunch: true
PrivateSubnet1: # ECS・RDS用(パブリックIPなし)
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.11.0/24
AvailabilityZone: ap-northeast-1a
MapPublicIpOnLaunch: false
RDSSecurityGroup: # ECSからのみ5432を許可
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: RDS Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref ECSSecurityGroupIAM — 最小権限の原則
最小権限の原則(Principle of Least Privilege): 必要最小限の権限だけを付与する。
ECSTaskRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal: { Service: ecs-tasks.amazonaws.com }
Action: sts:AssumeRole
Policies:
- PolicyName: EchoTaskAppPolicy
PolicyDocument:
Statement:
- Effect: Allow # S3: 特定バケットのみ
Action: [s3:GetObject, s3:PutObject]
Resource: "arn:aws:s3:::echo-task-uploads/*"
- Effect: Allow # SSM: 特定パスのパラメータのみ
Action: [ssm:GetParameter, ssm:GetParameters]
Resource: "arn:aws:ssm:ap-northeast-1:*:parameter/echo-task/production/*"
- Effect: Allow # SQS: 特定キューのみ
Action: [sqs:SendMessage, sqs:ReceiveMessage, sqs:DeleteMessage]
Resource: !GetAtt SQSQueue.Arn# IAMシミュレーターで意図しない権限がないかテスト
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789:role/ecs-task-role \
--action-names "s3:DeleteBucket" \
--resource-arns "arn:aws:s3:::echo-task-uploads"
# 期待値: {"EvalDecision": "explicitDeny"}Secrets Manager — シークレット管理
パスワードやAPIキーをコードやenvファイルに直書きするのは危険だ。
# 悪い例
# config/database.yml(DBパスワードが平文)
production:
password: "super_secret_password_123"
# 良い例: AWS Secrets Managerから取得
# config/initializers/secrets.rb
class SecretsManager
def self.fetch(secret_name)
client = Aws::SecretsManager::Client.new(region: 'ap-northeast-1')
response = client.get_secret_value(secret_id: secret_name)
JSON.parse(response.secret_string)
rescue => e
Rails.logger.error("Failed to fetch secret: #{e.message}")
raise
end
end
# config/database.yml
production:
url: <%= ENV.fetch('DATABASE_URL') { SecretsManager.fetch('echo-task/db')['url'] } %># シークレットの作成
aws secretsmanager create-secret \
--name "echo-task/production/db" \
--secret-string '{"url":"postgresql://app_user:password@rds.host/echo_task_production"}'
# 30日ごとに自動ローテーション
aws secretsmanager rotate-secret \
--secret-id "echo-task/production/db" \
--rotation-lambda-arn "arn:aws:lambda:ap-northeast-1:123456789:function:RotateSecret" \
--rotation-rules '{"AutomaticallyAfterDays":30}'ペネトレーションテスト・脆弱性診断の進め方
「守れた」と思っているだけでは不十分だ。実際に攻撃してみて確認する。
主なツールと手順
# 1. OWASP ZAP(Webアプリの動的スキャン)
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://staging.echotask.app \
-r zap_report.html
# 2. Brakeman(Railsコードの静的解析)
gem install brakeman
brakeman -p . --format json --output brakeman_report.json
# 3. bundle-audit(gemの既知脆弱性チェック)
gem install bundler-audit
bundle audit check --update
# 4. Trivy(コンテナイメージのCVEスキャン)
trivy image --severity HIGH,CRITICAL \
echotask/app:latest
# 5. tfsec(Terraform/CloudFormationの設定ミスチェック)
tfsec ./infra/cloudformation/ペネトレーションテストのフロー
| ステップ | 内容 |
|---|---|
| スコープ定義 | 対象エンドポイント・機能を合意する |
| 偵察 | エンドポイント一覧・技術スタックの洗い出し |
| スキャン | 自動ツールで既知脆弱性を検出 |
| 手動テスト | 認証バイパス・権限昇格・ロジック欠陥を確認 |
| レポート | CVSSスコアで優先度付けし再現手順と一緒に報告 |
| 修正・再テスト | 修正後に同じテストを繰り返して確認 |
INFO
AWSペネトレーションテストのルール。 EC2・RDS・CloudFrontへのペネトレーションテストは事前申請なしに実施してよい(2024年現在)。 ただしDDoS・DNSハイジャックは禁止。AWS公式の「侵入テストポリシー」を確認してから実施する。
AWS WAF — Webアプリファイアウォール
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
Name: echo-task-waf
Scope: CLOUDFRONT
DefaultAction:
Allow: {}
Rules:
# AWSマネージドルール(共通の攻撃パターンを自動ブロック)
- Name: AWSManagedRulesCommonRuleSet
Priority: 1
OverrideAction: { None: {} }
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesCommonRuleSet
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: CommonRuleSet
# SQLインジェクション対策
- Name: AWSManagedRulesSQLiRuleSet
Priority: 2
OverrideAction: { None: {} }
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesSQLiRuleSet
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: SQLiRuleSet
# IPレートリミット(5分間で2000リクエスト)
- Name: RateLimitRule
Priority: 3
Action: { Block: {} }
Statement:
RateBasedStatement:
Limit: 2000
AggregateKeyType: IP
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: RateLimitセキュリティ強化の結果
Before: セキュリティ後回し
- SQLインジェクション試みが成功するケースあり
- DBパスワードが環境変数(デプロイスクリプトに平文)
- アカウントロックなし(ブルートフォースし放題)
After: 多層防御 + 継続的監視
- WAFが月平均4万件の攻撃をブロック
- Secrets ManagerでKMS暗号化されたシークレット管理
- devise: 5回失敗でアカウントロック + 2FA対応
- GuardDutyが不審なAPIコールを自動検知・Slack通知
- Brakemanで週次静的解析をCIに組み込み
- セキュリティインシデント: 0件(3ヶ月継続中)
ハルトは攻撃ログをSlackで眺めながら、コーヒーを飲んだ。 「4万件ブロックされてる。WAFがいてよかった。」
セキュリティは完成しない。攻撃者は常に新しい手口を開発する。 だからこそ継続的な監視・定期的な診断・チームの意識が重要だ。
ハルトはチームに新しいエンジニアを採用し、設計の相談を受けるようになった。
「どうやってシステム設計の問いに答えればいいのか?設計面接での戦い方を教えてほしい。」
最終章では、システム設計の思考法をまとめる。
INFO
この章のキーポイント
- OWASP Top 10(アクセス制御・インジェクション・認証)をRailsで対策する
- 認証(誰か?)と認可(何を許可?)は別の問題。Cognitoでユーザー管理を委譲する
- KMSのエンベロープ暗号化でフィールドレベルまで守る
- GuardDuty + SecurityHubで脅威検知と設定ミスを自動化する
- セキュリティヘッダー(CSP・HSTS)はRailsとGoで数行で設定できる
- ペネトレーションテストで「守れているか」を実際に確認する