非機能 (3) セキュリティ ─ Zero Trust と Tenant 分離
セキュリティは利用者ドメインでも当然重要だが、共通基盤では 横断性 が桁違いに重い。1 つの脆弱性が 全利用者に波及 する。1 つのテナントの情報漏洩が 全テナントの信頼を破壊 する。
この章で扱う 4 つの判断軸:
- Zero Trust の前提
- Tenant 分離
- Policy as Code(認可ロジックの外部化)
- 最小権限と監査
Zero Trust ─ 内部だから安全、ではない
伝統的なセキュリティモデルは 境界防御(Perimeter Defense):
インターネット | ファイアウォール | 内部ネットワーク
↑ ここに来たら信頼する
これが破綻するのは:
- VPC 内で横断的な攻撃(lateral movement)
- 内部不正(社員 / 委託先)
- 侵害されたサービス が他を攻撃
Zero Trust の前提:
誰も信頼しない。リクエスト単位で認証 / 認可 / 監査する。
graph TB
C[Client / Service] -->|every request| Auth[Authenticate]
Auth --> Authz[Authorize]
Authz --> Audit[Audit log]
Audit --> R[Resource]
Note1[内部からのリクエストでも<br/>必ず全 step を通る]
style Auth fill:#e1f5ff
style Authz fill:#fff4e1
style Audit fill:#ffe1e1
サービス間認証:mTLS
サービス間通信は mutual TLS (mTLS) で:
サービス A → サービス B
両方が証明書を提示し、相互検証
→ A は B の身元を確認、B は A の身元を確認
→ どちらかが成りすましだと拒否
Service Mesh(Istio / Linkerd)が mTLS を自動化する。これは共通基盤側でインフラとして提供する典型例。利用者は明示的に意識しなくていい。
サービス間認可:SPIFFE / SPIRE
サービスごとに SVID (Secure Production Identity) を発行し、認証 / 認可に使う:
# 例:SPIFFE ID
spiffe://example.com/notify-service
# notification API は notify-service だけが呼べる
これにより「どのサービスがどのサービスを呼べるか」を明示的に制御できる。
Tenant 分離 ─ 漏れない設計
マルチテナント基盤では、1 つのテナントのデータが他のテナントに見えないことが必須。
分離レベル(再確認)
前作 DB 設計の軸 2026 ch16 で扱った 5 段階:
| 戦略 | 分離度 | 漏洩リスク |
|---|---|---|
| Cluster-per-tenant | 最強 | 物理的に不可能 |
| Database-per-tenant | 強 | 設定ミス時のみ |
| Schema-per-tenant | 中 | アプリ側のバグで漏洩可能性 |
| Row-level filtering(RLS) | 弱 | アプリのバグで全テナント漏洩 |
| Tenant prefix in PK | 弱 | アプリのバグで漏洩 |
分離度を 1 段下げると、漏洩リスクは桁違いに上がる。
コードレベルの隔離
弱い分離(RLS / prefix)を選んだ場合、コードで補強する:
// ✅ tenant context を必須にする
class OrderRepository {
// ❌ allowed
async findById(id: OrderId): Promise<Order | null>;
// ✅ 強制的に tenant_id を渡す
async findById(tenantContext: TenantContext, id: OrderId): Promise<Order | null>;
}
tenantContext を関数引数で必須にする ことで、書き忘れを型システムで防ぐ。
Postgres RLS の活用
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- アプリ側
SET LOCAL app.tenant_id = '...';
SELECT * FROM orders; -- 自動的に tenant フィルタが効く
SQL に WHERE tenant_id を書き忘れる事故を物理的に防げる。共通基盤の DB ではほぼ必須。
Policy as Code ─ 認可ロジックの外部化
認可は基盤の 本業 だが、ロジックは複雑。これを コード化 して外部にする。
OPA(Open Policy Agent)
# rego 言語で policy を書く
package authz
default allow = false
allow {
input.method == "GET"
input.path == "/v1/orders"
input.user.role == "admin"
}
allow {
input.method == "GET"
input.path == sprintf("/v1/orders/%s", [order_id])
input.user.id == data.orders[order_id].owner_id
}
アプリケーション側は OPA に判定を委譲:
const decision = await opa.evaluate({
input: { user, method, path },
});
if (!decision.allow) {
return { status: 403 };
}
メリット:
- ポリシーがコードと分離 され、変更が独立
- ポリシーをテストできる
- 監査者にポリシーだけを見せられる
AWS Cedar
Cedar は AWS の policy language。SPIFFE / IAM / 一般的な認可に使える。
permit (
principal in Group::"admin",
action == Action::"read",
resource in Folder::"company-public"
);
型安全な policy が特徴。schema を定義し、ポリシーが schema に合致しない場合は load 時にエラー。
AuthZed / SpiceDB
Google の Zanzibar 論文を実装した relationship-based 認可:
// ユーザー u が、document d の reader として許可されている
relationship: u#reader@d
// チェック
check: u, document:d, action: read
→ allowed
「誰が誰に対してどう関係しているか」 を中心にした認可。複雑な権限階層(Google Drive のフォルダ共有等)に強い。
最小権限の原則
与える権限は、必要最小限に。これは Linux 時代から変わらない原則。
サービス間
# サービス A が DB に対して持つ権限
db_role: read_only
allowed_tables:
- orders # SELECT のみ
- customers # SELECT のみ
denied_tables:
- audit_log # 触らせない
- payment_tokens # 触らせない
サービスごとに role を分ける。1 つのサービスが侵害されても、他テーブルへの被害が広がらない。
Token の scope
// ❌ admin token を無制限に発行
const token = issueAdminToken(user); // 全権限
// ✅ scope を明示
const token = issueToken(user, {
scopes: ['read:orders', 'write:orders'],
expiresIn: '15m',
});
短い期限 + 限定された scope。token が漏れても被害は最小化される。
Secret 管理
API key / DB password / TLS 証明書 を コードに混ぜない。
graph TB
App[Application] -->|fetch| SM[Secret Manager<br/>HashiCorp Vault / AWS Secrets Manager]
SM -.rotation.- KMS[KMS]
Note1[Secret はコード外で管理<br/>定期 rotation 必須]
style SM fill:#e1f5ff
Rotation の自動化が肝心。月単位で自動的に rotate され、コード変更不要。
監査ログ
「誰が何をいつしたか」を不可逆に残す。前作 DB 設計の軸 2026 ch16 の 判断 5:監査・バージョニング を参照。
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL,
actor_id UUID NOT NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
before JSONB,
after JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- INSERT only、UPDATE / DELETE しない
Append-only、UPDATE / DELETE 不可。これが Pat Helland の Outside data そのもの。
セキュリティの “桁違い” 設計
graph TB
L1[Network: mTLS / Service Mesh]
L2[Identity: SPIFFE / OIDC]
L3[AuthZ: Policy as Code]
L4[Tenant: 物理 / 論理分離]
L5[Data: 暗号化 at rest / in transit]
L6[Audit: Append-only log]
L7[Secret: Vault + Rotation]
L1 --> L2 --> L3 --> L4 --> L5 --> L6 --> L7
Note1[7 層を全部やる必要がある]
style L1 fill:#e1f5ff
style L7 fill:#e1f5ff
利用者ドメインなら 1-2 層で十分なところを、共通基盤は 7 層 すべて意識する必要がある。これが「桁が違う」と感じる正体。
この章の要点
- Zero Trust:内部からも信頼しない、リクエスト単位で auth + authz + audit
- mTLS / SPIFFE で サービス間の身元 を確立
- Tenant 分離はコードと DB の両層で。RLS は強力
- Policy as Code(OPA / Cedar / AuthZed)で認可ロジックを外部化
- 最小権限:サービス role 分割、token scope 限定、短期 expire
- Secret 管理は Vault + 自動 rotation
- 監査ログは Append-only、Outside data として扱う
- 共通基盤のセキュリティは 7 層 すべての設計が必要
次章への問いかけ
内部は守れた。だが 何が起きているか が見えなければ、攻撃も性能劣化も気づけない。
次章で 非機能要件 (4) 可観測性 ── Distributed tracing、Metrics、Logs の三位一体。