mybook

セキュリティの基礎 — システムを守る

攻撃が来た日

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つの壁ではなく、複数の層で守る。

Loading diagram...

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
end

A03: インジェクション(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_sqlexecute を使うときは特に注意。 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でのソーシャルログインを導入した。 トークン発行の流れをシーケンス図で整理する。

Loading diagram...

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は「エンベロープ暗号化」を採用している。

  1. CMKでデータキーを生成する
  2. データキーで実際のデータを暗号化する
  3. 暗号化されたデータキーをデータと一緒に保存する
  4. 復号時は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: SlackNotify

GuardDutyが検知する主な脅威:

脅威の種類検知例
UnauthorizedAccess異常な国からのAPIコール
BackdoorC&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 ECSSecurityGroup

IAM — 最小権限の原則

最小権限の原則(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で数行で設定できる
  • ペネトレーションテストで「守れているか」を実際に確認する