mybook

HTTPS と TLS — 通信を暗号化する

インシデントの翌朝、リョウはまずログを全件見直した。攻撃者はAPIエンドポイントへのリクエスト内容を正確に把握していた。なぜだろうか。

調査を進めると、社内開発環境のAPIがHTTPで通信していることが判明した。そして本番環境でも、ロードバランサーとECSコンテナ間の通信が平文だったことがわかった。

「通信を覗き見されていた可能性がある」

リョウは背筋が凍った。

TLSとは何か

TLS(Transport Layer Security)は、ネットワーク通信を暗号化するプロトコルだ。HTTPSは「HTTP over TLS」の略であり、すべてのWebトラフィックを暗号化する。

Loading diagram...

TLSが提供する3つの保証:

  1. 機密性 — 通信内容を第三者が読めない
  2. 完全性 — 通信途中でデータが改ざんされていない
  3. 認証 — 通信相手が本物であることを確認

INFO

TLS 1.3(2018年標準化)は、TLS 1.2と比べてハンドシェイクが1往復少なく、ラウンドトリップが減少します。また、Forward Secrecyが必須になり、過去の通信も保護されます。

AWS ACMで証明書を取得する

リョウの環境はAWSを使っていたので、AWS Certificate Manager(ACM)で無料のSSL証明書を取得した。

# ACMで証明書をリクエスト(DNS検証方式)
aws acm request-certificate \
  --domain-name "api.stockflow.example.com" \
  --subject-alternative-names "*.stockflow.example.com" \
  --validation-method DNS \
  --region ap-northeast-1
 
# 出力例
{
  "CertificateArn": "arn:aws:acm:ap-northeast-1:123456789012:certificate/abc123"
}
# 証明書の検証レコードを取得
aws acm describe-certificate \
  --certificate-arn "arn:aws:acm:ap-northeast-1:123456789012:certificate/abc123" \
  --query "Certificate.DomainValidationOptions"
 
# Route 53に検証レコードを自動追加
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890ABC \
  --change-batch file://validation-records.json

INFO

ACMの証明書はALB、CloudFront、API GatewayなどのAWSサービスで直接使用できます。証明書の自動更新も行われるため、証明書期限切れの事故を防げます。

ALBでHTTPSを強制する

証明書を取得したら、Application Load Balancer(ALB)でHTTPSを強制する。

# terraform/alb.tf
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.main.arn
 
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api.arn
  }
}
 
# HTTPからHTTPSへのリダイレクト
resource "aws_lb_listener" "http_redirect" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"
 
  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

ELBSecurityPolicy-TLS13-1-2-2021-06 を選択することで、TLS 1.3と1.2のみを許可し、古い脆弱なバージョンを拒否できる。

RailsでHSTSを設定する

HSTS(HTTP Strict Transport Security)は、ブラウザに「このドメインは常にHTTPSで接続せよ」と指示するHTTPヘッダーだ。

# config/environments/production.rb
Rails.application.configure do
  # HTTPSを強制
  config.force_ssl = true
 
  # HSTSの設定(デフォルト値を強化)
  config.ssl_options = {
    hsts: {
      expires: 1.year,
      subdomains: true,
      preload: true
    },
    redirect: {
      status: 301,
      body: "Moved Permanently"
    }
  }
end

これにより、Railsは自動的に以下のヘッダーを付与する。

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

WARNING

preloadフラグを有効にしてHSTSリストに登録すると、ブラウザはHTTP接続を完全に拒否します。一度登録すると削除に時間がかかるため、本番環境で問題ないことを確認してから設定してください。

TLSの設定を検証する

設定後、リョウは複数の方法で設定を検証した。

# opensslで証明書を確認
openssl s_client -connect api.stockflow.example.com:443 -tls1_3 2>&1 | head -30
 
# TLSバージョンとCipher Suiteを確認
curl -v --tlsv1.3 https://api.stockflow.example.com/health 2>&1 | grep -E "TLS|SSL|cipher"
 
# SSL Labsのテスト(コマンドライン版)
sslyze api.stockflow.example.com
# 出力例(良好な設定)
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256

証明書の有効期限監視

証明書の期限切れは深刻なインシデントになりうる。ACMは自動更新するが、外部の証明書を使う場合は監視が必要だ。

# config/initializers/ssl_certificate_check.rb
# CloudWatchにメトリクスを送る(カスタム監視)
class SslCertificateChecker
  def self.check(domain)
    cert = fetch_certificate(domain)
    days_until_expiry = (cert.not_after - Time.now) / 86400
 
    if days_until_expiry < 30
      # SNSでアラートを送信
      Aws::SNS::Client.new.publish(
        topic_arn: ENV['ALERT_TOPIC_ARN'],
        message: "SSL certificate for #{domain} expires in #{days_until_expiry.round} days",
        subject: "[WARNING] SSL Certificate Expiring Soon"
      )
    end
 
    days_until_expiry
  end
 
  private
 
  def self.fetch_certificate(domain)
    tcp = TCPSocket.new(domain, 443)
    ssl = OpenSSL::SSL::SSLSocket.new(tcp)
    ssl.hostname = domain
    ssl.connect
    cert = ssl.peer_cert
    ssl.close
    cert
  end
end
# config/schedule.yml(whenevergemの設定)
every 1.day, at: '9:00 am' do
  runner "SslCertificateChecker.check('api.stockflow.example.com')"
end

API間通信の暗号化

本番環境のECSコンテナ間の通信も暗号化する必要がある。

Loading diagram...
# docker-compose.production.yml(概念例)
services:
  api:
    environment:
      DATABASE_URL: "postgresql://user:pass@db.internal:5432/stockflow?sslmode=verify-full&sslrootcert=/etc/ssl/certs/rds-ca.pem"
      REDIS_URL: "rediss://redis.internal:6380/0"  # rediss:// でTLS有効
# config/database.yml
production:
  adapter: postgresql
  url: <%= ENV['DATABASE_URL'] %>
  # SSLモードの強制
  sslmode: verify-full
  sslrootcert: /etc/ssl/certs/rds-ca.pem

WARNING

sslmode: verify-fullは最も厳格な設定で、証明書の有効性と証明書のCNがホスト名と一致することを確認します。sslmode: requireだけでは中間者攻撃を防げません。

セキュリティヘッダーの追加

TLSに加えて、追加のセキュリティヘッダーを設定する。

# config/initializers/security_headers.rb
Rails.application.config.middleware.use Rack::Attack
 
# Rackミドルウェアでセキュリティヘッダーを追加
class SecurityHeaders
  def initialize(app)
    @app = app
  end
 
  def call(env)
    status, headers, body = @app.call(env)
 
    headers.merge!(
      # XSSフィルタを有効化
      'X-XSS-Protection' => '1; mode=block',
      # コンテンツタイプのスニッフィングを防止
      'X-Content-Type-Options' => 'nosniff',
      # クリックジャッキングを防止
      'X-Frame-Options' => 'DENY',
      # 参照元情報を制限
      'Referrer-Policy' => 'strict-origin-when-cross-origin',
      # 権限ポリシー
      'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()'
    )
 
    [status, headers, body]
  end
end
 
Rails.application.config.middleware.insert_before 0, SecurityHeaders

リョウの気づき

夜通し作業してHTTPSを完全に設定したリョウは、ひとつの重要な教訓を得た。

「通信の暗号化は基礎中の基礎だ。でも、これだけでは攻撃者を防げない。次は、通信している相手が本当に正当なユーザーかどうかを確認する方法が必要だ」

TLSは「通信路を守る」ものだ。「誰が通信しているか」を守るのは、認証の仕事になる。

チェックリスト

  • 全エンドポイントでHTTPSを強制している
  • HTTPからHTTPSへのリダイレクト(301)が設定されている
  • TLS 1.2以上のみ許可している(TLS 1.0/1.1は無効化)
  • HSTSヘッダーが設定されている(max-ageは最低1年)
  • ACMまたは同等の証明書自動更新が設定されている
  • 証明書の有効期限監視が設定されている
  • 内部通信(DB、Redis、マイクロサービス間)も暗号化されている
  • TLS設定をSSL Labsなどでスコアリングしている(A以上を目指す)
  • セキュリティヘッダーが適切に設定されている