Load Balancer + Cloud Armor + Cloud DNSPublic entrance with WAF
松葉の公開エンドポイントは 本体 portal.example.com と 共有リンク受取専用 inbox.portal.example.com の 2 ホスト構成です。1 つの Load Balancer でホストごとにバックエンドを振り分け、2 種類の Cloud Armor ポリシーで本体と共有リンクのレート制限を独立に当てました。
Cloud Armor は Google の WAF(Web Application Firewall)+ IP レート制限サービスです。OWASP(Open Web Application Security Project)が選定する 6 種の代表的攻撃(SQL インジェクション・クロスサイトスクリプティング 等)をマネージドルールで検出し、しきい値超過アクセスを 429 で拒否します。
2 ホストの役割分担Two-host split
| ホスト | 用途 | Cloud Armor |
|---|---|---|
portal.example.com | 本体(サインイン・Box 操作・管理画面・API・Webhook) | 本体ポリシー(IP ごとのレート制限) |
inbox.portal.example.com | 共有リンク受取専用(/s/[token] + /api/share-links/*)。ログイン不要 | 共有リンクポリシー(IP ごとのレート制限) |
ホストを分ける理由: 共有リンク経路は「受信者がログインしていない状態でアクセスする」ため、本体とは攻撃面が異なります。Cookie 領域を分離(Host-only Cookie)、Cloud Armor のレート制限を独立、URL Map で /api/share-links/* 以外は到達不能、と「本体 API 全体を公開しない」を構造で保証しました。
Load Balancer の構造1 LB · 2 NEG · 4 backend services
Cloud Run を LB のバックエンドに繋ぐ仕組みは「Serverless NEG(ネットワークエンドポイントグループ)」と呼ばれます。NEG は matsuba-web と matsuba-api の 2 つを新規作成し、本体と共有リンクで NEG を 再利用することでデプロイ管理を 1 本化しています。Cloud Armor は「バックエンドサービス単位」でしか付けられないため、ポリシーを分けるためにバックエンドサービスを 4 個に分けています。
2 NEG × 4 バックエンドサービスの工夫: Cloud Run の実体は matsuba-web と matsuba-api の 2 つだけで足ります。それに対してバックエンドサービスを 4 個に増やしたのは「Cloud Armor のポリシーをバックエンドサービス単位で付ける」仕様のため。同じ NEG を 2 つのバックエンドサービスから参照することで、デプロイ管理は 2 個のままで、ポリシーだけ分けられます。
URL Map(ホスト + パス → バックエンド)Host + path routing
| ホスト | パス | バックエンド |
|---|---|---|
portal.example.com | /api/* · /internal/* | bs-matsuba-api |
portal.example.com | 上記以外 | bs-matsuba-web-main |
inbox.portal.example.com | /api/share-links/* | bs-matsuba-api-share |
inbox.portal.example.com | 上記以外 | bs-matsuba-web-share |
Cloud Armor ポリシー · 各 7 ルール × 2 ポリシーWAF rules
OWASP のマネージドルールセットを「詳細モード」で適用しました。詳細モードでは CEL 式(Common Expression Language、Google が定義する条件式言語)で柔軟に条件を書けます。本構築ではすべて sensitivity: 1(高精度、誤検知少)で開始しています。
| 優先度 | ルール | CEL 式 | アクション |
|---|---|---|---|
| 1000 | SQL インジェクション検出 | evaluatePreconfiguredWaf('sqli-v422-stable', {'sensitivity': 1}) | 403 拒否 |
| 1001 | XSS(クロスサイトスクリプティング)検出 | evaluatePreconfiguredWaf('xss-v422-stable', {'sensitivity': 1}) | 403 拒否 |
| 1002 | LFI(ローカルファイル参照)検出 | evaluatePreconfiguredWaf('lfi-v422-stable', {'sensitivity': 1}) | 403 拒否 |
| 1003 | RFI(リモートファイル参照)検出 | evaluatePreconfiguredWaf('rfi-v422-stable', {'sensitivity': 1}) | 403 拒否 |
| 1004 | RCE(リモートコード実行)検出 | evaluatePreconfiguredWaf('rce-v422-stable', {'sensitivity': 1}) | 403 拒否 |
| 1005 | スキャナ検出 | evaluatePreconfiguredWaf('scannerdetection-v422-stable', {'sensitivity': 1}) | 403 拒否 |
| 2000 | IP レート制限(throttle) | true(全トラフィック) | IP ごとのレート制限(本体と共有リンクで別々の上限)、超過は 429 |
「許可リスト型」を取らない理由: 社外取引先のアクセスを受け入れるため、IP やアカウントで「事前に登録された人のみ許可」とはできません。OWASP ルールと IP レート制限の「許可をデフォルトに、攻撃パターンと暴走 IP だけを Deny」方式で防御します。誤検知が出た場合は CEL 式で個別シグネチャを opt_out_rule_ids で除外する設計です。
DNS と SSL 証明書DNS & cert
| 項目 | 値 |
|---|---|
| ドメイン | portal.example.com(Cloud Domains で取得済) |
| DNS ゾーン | Cloud DNS portal-example-com |
| A レコード | apex(portal.example.com)+ inbox(inbox.portal.example.com)の 2 件、どちらも LB の静的 IP を指す |
| SSL 証明書 | Google マネージド SSL を 1 枚(SAN: portal.example.com + inbox.portal.example.com) |
| HSTS | 本体ドメイン はブラウザの HSTS preload の対象(常時 HTTPS)。LB でも Strict-Transport-Security: max-age=63072000; includeSubDomains; preload を送出 |