目次を表示する

共通基盤の設計軸 2026 ─ 抽象・責務・非機能要件を設計する 15 章

非機能 (3) セキュリティ ─ Zero Trust と Tenant 分離

非機能 (3) セキュリティ ─ Zero Trust と Tenant 分離

セキュリティは利用者ドメインでも当然重要だが、共通基盤では 横断性 が桁違いに重い。1 つの脆弱性が 全利用者に波及 する。1 つのテナントの情報漏洩が 全テナントの信頼を破壊 する。

この章で扱う 4 つの判断軸:

  1. Zero Trust の前提
  2. Tenant 分離
  3. Policy as Code(認可ロジックの外部化)
  4. 最小権限と監査

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-onlyUPDATE / 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 の三位一体。