コンテンツにスキップ

ファイアウォール & データプロテクション

対象フェーズ: PoC/Beta(段階的に強化)
最終更新: 2026-04-19
ステータス: 承認待ち

ポリシー準拠

本ドキュメントは最新インフラポリシーに準拠しています。Beta 基盤は XServer VPS + CoreServerV2 CORE+X、AWS 利用は Cognito のみ。認証 JWT 検証は Cognito(AWS SDK 経由)、MySQL は MariaDB 互換スキーマを維持、バックアップは Garage (S3互換 OSS) または OCI Object Storage に保管。


1. セキュリティアーキテクチャ概要

1.1 PoC/Beta セキュリティレイヤー

                        Internet
                    ┌──────▼──────┐
                    │  Cloudflare │  ← Layer 1: CDN + DDoS + WAF
                    │  (Free Plan)│
                    └──────┬──────┘
                    ┌──────▼──────┐
                    │ XServer VPS │  ← Layer 2: OS Firewall
                    │ iptables /  │  (6 core / 10 GB)
                    │ ufw         │
                    └──────┬──────┘
                    ┌──────▼──────┐
                    │  Traefik    │  ← Layer 3: Reverse Proxy
                    │  (TLS/Rate  │     TLS終端 + Rate Limiting
                    │   Limiting) │
                    └──────┬──────┘
              ┌────────────┼────────────┐
              │            │            │
        ┌─────▼────┐ ┌────▼─────┐ ┌────▼─────┐
        │ Go Auth  │ │ Go Core  │ │ Go Event │  ← Layer 4: App Security
        │ (JWT)    │ │ (RBAC)   │ │ (AuthZ)  │     認証・認可・バリデーション
        └─────┬────┘ └────┬─────┘ └────┬─────┘
              │            │            │
        ┌─────▼────────────▼────────────▼─────┐
        │     MySQL 8.0 (MariaDB互換スキーマ)      │  ← Layer 5: Data Security
        │    (暗号化 at rest + TLS接続)             │     暗号化 + アクセス制御
        └─────────────────────────────────────┘

2. Layer 1: Cloudflare(CDN + DDoS + WAF)

2.1 なぜ Cloudflare Free Plan か

機能 Cloudflare Free 備考
DDoS防御 無制限・無料 L3〜L7 まで無料で防御
WAF ルール 5 カスタムルール Beta フェーズはこの範囲で十分
SSL/TLS 無料 CDN 経由で TLS 提供
Rate Limiting 1ルール無料 基本的な防御は無料枠で可能
月額コスト ¥0 AWS WAF / Shield は採用しない(ポリシー)

2.2 Cloudflare 設定

DNS設定:
  api.recerdo.app  → A  → VPS_IP (Proxied: ON)
  app.recerdo.app  → A  → VPS_IP (Proxied: ON)

SSL/TLS:
  暗号化モード: Full (Strict)
  最小TLSバージョン: TLS 1.2
  自動HTTPS書き換え: ON
  HSTS: ON (max-age=31536000, includeSubDomains)

Firewall Rules (5ルール上限):
  1. Block: 既知の悪意あるBot (cf.client.bot AND NOT cf.client.bot)
  2. Challenge: 非日本・非米国からの/api/auth/* アクセス
  3. Block: /admin/* への外部アクセス
  4. Rate Limit: /api/auth/login → 10 req/min per IP
  5. Block: User-Agent が空のリクエスト

Security Level: Medium
Bot Fight Mode: ON
Browser Integrity Check: ON

2.3 Cloudflare → Origin 通信の保護

# Traefik / nginx で Cloudflare の IP のみ許可
# Cloudflare IP ranges: https://www.cloudflare.com/ips/

# 例: /etc/nginx/conf.d/cloudflare-only.conf
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;

3. Layer 2: OS ファイアウォール(ufw / iptables)

3.1 ufw 設定

# デフォルトポリシー
ufw default deny incoming
ufw default allow outgoing

# SSH(管理用 — IP制限推奨)
ufw allow from YOUR_ADMIN_IP to any port 22

# HTTP/HTTPS(Cloudflare経由)
ufw allow 80/tcp
ufw allow 443/tcp

# 他のポートはすべてブロック
# MySQL (3306), Redis (6379) は外部公開しない

ufw enable

3.2 SSH ハードニング

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
AllowUsers deploy
Protocol 2

# fail2ban 設定
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 22
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600

3.3 Docker ネットワーク分離

# docker-compose.yml
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # 外部アクセス不可

services:
  nginx:
    networks:
      - frontend
      - backend

  core-svc:
    networks:
      - backend  # nginx経由でのみアクセス可能

  mysql:
    networks:
      - backend  # Go サービスからのみアクセス可能

  redis:
    networks:
      - backend

4. Layer 3: nginx セキュリティ設定

4.1 TLS 設定

# /etc/nginx/conf.d/ssl.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

4.2 セキュリティヘッダー

# /etc/nginx/conf.d/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.recerdo.app wss://api.recerdo.app" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

4.3 Rate Limiting

# /etc/nginx/conf.d/rate-limit.conf

# ゾーン定義
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=ws:10m rate=2r/s;

# 接続数制限
limit_conn_zone $binary_remote_addr zone=connlimit:10m;

server {
    # 認証エンドポイント(ブルートフォース防止)
    location /api/auth/login {
        limit_req zone=auth burst=3 nodelay;
        limit_conn connlimit 5;
        proxy_pass http://auth-svc:8080;
    }

    # 一般 API
    location /api/ {
        limit_req zone=api burst=50 nodelay;
        limit_conn connlimit 20;
        proxy_pass http://core-svc:8080;
    }

    # ファイルアップロード
    location /api/storage/upload {
        limit_req zone=upload burst=2 nodelay;
        client_max_body_size 10m;
        proxy_pass http://storage-svc:8080;
    }

    # WebSocket
    location /ws/ {
        limit_req zone=ws burst=5 nodelay;
        proxy_pass http://core-svc:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400s;
    }
}

5. Layer 4: アプリケーションセキュリティ

5.1 JWT 検証ミドルウェア

// middleware/auth.go — AWS Cognito JWT (RS256) 検証
// AWS 利用はポリシー上 Cognito のみ
package middleware

import (
    "context"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/lestrrat-go/jwx/v2/jwk"
    "github.com/lestrrat-go/jwx/v2/jwt"
)

type AuthMiddleware struct {
    jwks jwk.Set // Cognito JWKS (https://cognito-idp.<region>.amazonaws.com/<pool>/.well-known/jwks.json)
    issuer string
    audience string
}

func NewAuthMiddleware(ctx context.Context, jwksURL, issuer, audience string) (*AuthMiddleware, error) {
    set, err := jwk.Fetch(ctx, jwksURL)
    if err != nil {
        return nil, err
    }
    return &AuthMiddleware{jwks: set, issuer: issuer, audience: audience}, nil
}

func (m *AuthMiddleware) Authenticate() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized,
                gin.H{"error": "missing authorization header"})
            return
        }
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.AbortWithStatusJSON(http.StatusUnauthorized,
                gin.H{"error": "invalid authorization format"})
            return
        }
        tok, err := jwt.Parse([]byte(parts[1]),
            jwt.WithKeySet(m.jwks),
            jwt.WithIssuer(m.issuer),
            jwt.WithAudience(m.audience),
        )
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized,
                gin.H{"error": "invalid token"})
            return
        }
        c.Set("uid", tok.Subject())
        if email, ok := tok.Get("email"); ok {
            c.Set("email", email)
        }
        c.Next()
    }
}

5.2 入力バリデーション

// 全 API エンドポイントで binding タグによるバリデーション
type CreateEventRequest struct {
    Title       string `json:"title" binding:"required,min=1,max=200"`
    Description string `json:"description" binding:"max=2000"`
    StartDate   string `json:"start_date" binding:"required,datetime=2006-01-02T15:04:05Z07:00"`
    EndDate     string `json:"end_date" binding:"required,datetime=2006-01-02T15:04:05Z07:00,gtfield=StartDate"`
    Location    string `json:"location" binding:"max=500"`
    OrgID       uint   `json:"org_id" binding:"required,gt=0"`
}

// SQL インジェクション防御: GORM のパラメータバインディングを必須化
// NG: db.Where("name = '" + name + "'")
// OK: db.Where("name = ?", name)

5.3 RBAC(ロールベースアクセス制御)

// middleware/rbac.go
func RequireOrgRole(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        uid := c.GetString("uid")
        orgID := c.Param("org_id")

        var orgUser models.OrgUser
        if err := db.Where("user_uid = ? AND organization_id = ?",
            uid, orgID).First(&orgUser).Error; err != nil {
            c.AbortWithStatusJSON(http.StatusForbidden,
                gin.H{"error": "not a member of this organization"})
            return
        }

        roleAllowed := false
        for _, role := range roles {
            if orgUser.OrgRole == role {
                roleAllowed = true
                break
            }
        }

        if !roleAllowed {
            c.AbortWithStatusJSON(http.StatusForbidden,
                gin.H{"error": "insufficient permissions"})
            return
        }

        c.Set("org_role", orgUser.OrgRole)
        c.Next()
    }
}

6. Layer 5: データセキュリティ

6.1 暗号化

レイヤー 方式 実装
通信中 (in transit) TLS 1.2/1.3 Cloudflare + Traefik + MySQL TLS接続
保存時 (at rest) AES-256 MySQL (MariaDB互換スキーマ): InnoDB tablespace encryption
アプリケーション bcrypt / SHA-256 パスワード: bcrypt, 連絡先ハッシュ: SHA-256
バックアップ AES-256 mysqldump → gpg 暗号化 → Garage (Beta) / OCI Object Storage (本番)

6.2 MySQL 暗号化設定

-- InnoDB テーブルスペース暗号化を有効化
-- my.cnf
[mysqld]
early-plugin-load=keyring_file.so
keyring_file_data=/var/lib/mysql-keyring/keyring
innodb_encrypt_tables=ON
innodb_encrypt_log=ON

-- テーブル作成時に暗号化を指定
CREATE TABLE users (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    ...
) ENGINE=InnoDB ENCRYPTION='Y';

6.3 MySQL TLS 接続

# docker-compose.yml
mysql:
  command: >
    --require-secure-transport=ON
    --ssl-ca=/etc/mysql/ssl/ca.pem
    --ssl-cert=/etc/mysql/ssl/server-cert.pem
    --ssl-key=/etc/mysql/ssl/server-key.pem
// Go 接続文字列
dsn := "user:pass@tcp(mysql:3306)/recerdo?tls=true&parseTime=true"

6.4 機密データの取り扱い

データ種別 保存方法 アクセス制御
パスワード bcrypt (cost=12) ハッシュ 平文保存禁止
メールアドレス 平文(検索用) 本人 + 組織管理者のみ
電話番号 AES-256 暗号化 本人のみ
連絡先ハッシュ SHA-256 + SALT フレンド提案処理のみ
Cognito クライアント Secret 環境変数 アプリケーション内部のみ
Garage / OCI Object Storage アクセスキー 環境変数 or OCI Vault デプロイ環境のみ
SMTP 認証情報(Postfix Submission) 環境変数 Notification Svc のみ

7. バックアップ & リカバリ

7.1 PoC/Beta バックアップ戦略

対象 方式 頻度 保持期間 保存先
MySQL (MariaDB互換) mysqldump + gzip + gpg 日次 30日 Garage (CoreServerV2) → 本番は OCI Object Storage
Redis RDB スナップショット 6時間毎 7日 XServer VPS ローカル
アプリ設定 Git リポジトリ コミット毎 無期限 GitHub
メディア (Garage/OCI OSS) バージョニング 自動 30日 Garage (Beta) / OCI Object Storage (本番)

7.2 バックアップスクリプト

#!/bin/bash
# /opt/recerdo/scripts/backup-db.sh
# Garage (S3互換 OSS) にバックアップする。本番は同じスクリプトで
# STORAGE_ENDPOINT を OCI Object Storage に切替。
set -euo pipefail

BACKUP_DIR="/tmp/backup"
BUCKET="s3://recerdo-backups"
DATE=$(date +%Y%m%d_%H%M%S)
GPG_RECIPIENT="backup@recerdo.app"

# S3 互換 (Garage / OCI Object Storage) への接続情報
export AWS_ACCESS_KEY_ID="${STORAGE_ACCESS_KEY}"
export AWS_SECRET_ACCESS_KEY="${STORAGE_SECRET_KEY}"
ENDPOINT="${STORAGE_ENDPOINT}"   # Beta: https://garage.coreserver.example.com
                                  # Prod: https://objectstorage.ap-tokyo-1.oraclecloud.com

mkdir -p "$BACKUP_DIR"

# MySQL ダンプ (スキーマは MariaDB 互換)
docker exec recerdo-mysql mysqldump \
  --single-transaction \
  --routines \
  --triggers \
  --all-databases \
  | gzip \
  | gpg --encrypt --recipient "$GPG_RECIPIENT" \
  > "$BACKUP_DIR/mysql_${DATE}.sql.gz.gpg"

# S3 互換ストレージへアップロード(aws-cli は S3 互換 API に対して使える)
aws --endpoint-url "$ENDPOINT" s3 cp \
  "$BACKUP_DIR/mysql_${DATE}.sql.gz.gpg" \
  "$BUCKET/mysql/"

# 古いバックアップ削除(ローカル)
find "$BACKUP_DIR" -name "*.gpg" -mtime +7 -delete

echo "Backup completed: mysql_${DATE}.sql.gz.gpg"

7.3 RPO / RTO

指標 PoC/Beta Production (目標)
RPO (Recovery Point Objective) 24時間 1時間
RTO (Recovery Time Objective) 4時間 30分

8. セキュリティ運用

8.1 PoC/Beta で実施するセキュリティタスク

タスク 頻度 担当
OS セキュリティパッチ適用 週次 unattended-upgrades 自動
Docker イメージ更新 月次 手動 + Dependabot
依存パッケージ脆弱性スキャン CI/CD毎 govulncheck + npm audit
アクセスログレビュー 週次 手動
SSL証明書更新 自動 Let's Encrypt + certbot
バックアップ復元テスト 月次 手動

8.2 セキュリティチェックリスト

カテゴリ チェック項目 状態
ネットワーク Cloudflare Proxy 有効 [ ]
ネットワーク ufw ファイアウォール有効 [ ]
ネットワーク SSH 鍵認証のみ [ ]
ネットワーク MySQL/Redis 外部非公開 [ ]
TLS TLS 1.2+ 強制 [ ]
TLS HSTS ヘッダー設定 [ ]
認証 AWS Cognito JWT 検証 [ ]
認証 ログイン Rate Limiting [ ]
データ MySQL (MariaDB互換) at-rest 暗号化 [ ]
データ バックアップ暗号化 [ ]
データ 環境変数で機密管理 [ ]
Docker internal ネットワーク分離 [ ]
Docker non-root ユーザー実行 [ ]
監視 fail2ban 有効 [ ]
監視 異常アクセスアラート [ ]

9. PoC → Production セキュリティ強化パス

フェーズ 追加するセキュリティ コスト(概算)
PoC/Beta (現在) Cloudflare Free + ufw + Traefik + AWS Cognito + fail2ban ¥0
Growth Cloudflare Pro ($20/月) + OCI WAF ルール拡張 約 ¥3,000〜¥8,000/月
Production OCI WAF + OCI Vault(シークレット管理)+ OCI Bastion + 監査強化 OCI 利用料に内包

PoC/Beta のセキュリティ限界

Cloudflare Free の WAF は 5 ルールまで。高度な攻撃パターン(SQLi/XSS の詳細検知)には対応できないため、Growth フェーズで Cloudflare Pro + OCI WAF への移行が必要。AWS WAF / Shield は 採用しない(ポリシー)。


10. コンプライアンス考慮

10.1 PoC/Beta で最低限必要な対応

要件 対応
個人情報保護法(日本) プライバシーポリシー掲載 + データ利用目的明示
GDPR(EU ユーザー対象時) 同意取得 + データ削除リクエスト対応(手動)
SSL/TLS 必須 Cloudflare + Let's Encrypt で対応済み
パスワード安全保管 bcrypt ハッシュ化

10.2 Production で追加必要な対応

要件 対応
GDPR 自動データ削除 削除カスケード仕様(設計済み)の実装
データポータビリティ ユーザーデータエクスポート API
Cookie同意 Cookie Banner + 同意管理
インシデント対応 72時間以内通知プロセス
DPO (データ保護責任者) 指名(ユーザー数に応じて)