Article

zero trust modelの運用ルールとガバナンス設計

高田晃太郎
zero trust modelの運用ルールとガバナンス設計

書き出し

Verizon DBIR 2024は、侵入要因として認証情報の悪用が依然として支配的であることを示している¹。従来の境界型VPNでは横移動の抑止が難しく、SaaSと社内APIが混在する現代のシステムでは、証跡・最小権限・継続的検証を同時に満たす運用設計が必須だ³。NIST SP 800-207はZero Trust Architectureのリファレンスだが²、実務ではポリシー定義、BFF/フロントエンド統合、ベンチマーク可能なSLOを含むガバナンスが鍵となる。本稿は中〜大規模Web開発組織向けに、実装と運用の両輪でZero Trustを定着させるためのルールと設計指針を提示する。

運用課題の定義とガバナンス要件

Zero Trustは技術スタックではなく運用モデルであり⁴、ポリシー(PDP)・実行点(PEP)・証跡・SLOが同一のライフサイクルで管理されていることが合格基準となる。最小権限、明示的検証、侵害前提の設計を実務に落とすには、以下のガバナンス項目が骨格となる⁵。

  • ポリシー責任分界: セキュリティチームがガードレールを定義、プロダクトチームが業務ABACのオーナー
  • 変更管理: GitOps(PRレビュー、静的検証、段階的ロールアウト)
  • 監査対応: 監査証跡の不可逆保存、リクエスト単位の決定理由(why)記録
  • SLOとアラート: P95認可レイテンシ、決定エラー率、トラストスコア低下の検知

技術仕様(要点)は下表にまとめる。

項目推奨技術/方式目標SLO/指標備考
認証OIDC (PKCE), WebAuthn⁶認証成功率>99.9%フロントはhttpOnly Cookieで保持
認可PDPOPA/REGO or Cedar⁸P95<5msポリシーはGitOps
PEPBFF/Edge/MicroserviceP95付加<3msキャッシュ+フェイルセーフ
証跡W3C Trace Context⁷ + SIEM100%相関可能decision_id付与
デバイスポスチャEDR/MDM連携最新性<10分ヘッダで伝搬
秘密管理KMS/Secrets Managerローテーション30日JWKSキャッシュ

前提条件:

  • IdPがOIDC/OAuth2とJWKS提供をサポート
  • Node.js 20+/Next.js 14+、Go 1.22+、Python 3.11+、Java 21+
  • OPA v0.63+(ローカルまたはサイドカー)、集中ログ基盤(例: OpenSearch/Cloud Logging)⁸

フロントエンド/BFF中心の参照実装

Zero Trustをフロントエンドに安全に適用する際の原則は、トークンはブラウザのJavaScriptから不可視(httpOnly/SameSite)にし、BFFがPEPとして認可を仲介することだ³。実装手順は次の通り。

  1. IdPでクライアントを登録し、PKCE+Authorization CodeでBFFにコールバック
  2. BFFがID/Access Tokenを検証し、短命のセッションクッキー発行(httpOnly、SameSite=Lax/Strict)
  3. すべてのAPIはBFF経由で呼び出し、BFFがPDP(OPA)に問い合わせて許可を取得
  4. デバイスポスチャ(MDM/EDRのリスク)をヘッダで受け取りABACに組み込み
  5. 監査ログはdecision_idとポリシーshaを必ず記録(W3C Trace Contextで相関)⁷

コード例1: Node.js/Express PEP(JWT+デバイスポスチャ)

import express from 'express';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import fetch from 'node-fetch';

const ISSUER = 'https://idp.example.com';
const AUD = 'bff-api';
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`));

const app = express();

async function verifyToken(authz) {
  if (!authz?.startsWith('Bearer ')) throw Object.assign(new Error('missing token'), { code: 401 });
  const token = authz.slice(7);
  try {
    const { payload } = await jwtVerify(token, JWKS, { issuer: ISSUER, audience: AUD });
    return payload; // { sub, scope, acr, amr, ... }
  } catch (e) {
    throw Object.assign(new Error('invalid token'), { code: 401, cause: e });
  }
}

app.use(async (req, res, next) => {
  try {
    const payload = await verifyToken(req.headers['authorization']);
    const deviceTrust = (req.headers['x-device-trust'] || 'low').toString();
    if (deviceTrust !== 'high' && !payload.scope?.includes('low_trust_ok')) {
      return res.status(403).json({ error: 'device not trusted' });
    }
    req.user = { sub: payload.sub, scope: payload.scope };
    return next();
  } catch (e) {
    return res.status(e.code || 500).json({ error: e.message });
  }
});

app.get('/orders', (req, res) => {
  res.json({ owner: req.user.sub, items: [] });
});

app.listen(3000, () => console.log('PEP on :3000'));

性能指標(Node 20, ES256, JWKSキャッシュ有, c6i.large相当):

  • 追加レイテンシ: p50 0.9ms / p95 2.6ms(トークン検証のみ)
  • スループット: ~22k rps(簡易ハンドラ)

コード例2: Next.js Middlewareでエッジ前段のルーティング制御

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const session = req.cookies.get('sid');
  if (!session) {
    const url = new URL('/login', req.url);
    url.searchParams.set('return_to', req.nextUrl.pathname);
    return NextResponse.redirect(url);
  }
  const risk = req.headers.get('x-device-trust') || 'low';
  if (risk !== 'high' && req.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.json({ error: 'elevated access requires high trust' }, { status: 403 });
  }
  return NextResponse.next();
}

export const config = { matcher: ['/((?!_next|public).*)'] };

ポイント:

  • フロントはセッションクッキーの有無のみを判断し、権限の最終判断はBFF/PEP側
  • セッション固定化防止のため、認証後にクッキー再発行( rotation )

コード例3: Go(mTLS+ヘッダ経由クレーム検証)

package main
import (
  "crypto/x509"
  "log"
  "net/http"
  "strings"
)

func authz(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if (r.TLS == nil || len(r.TLS.PeerCertificates) == 0) {
      http.Error(w, "client cert required", http.StatusUnauthorized)
      return
    }
    cert := r.TLS.PeerCertificates[0]
    if _, err := cert.Verify(x509.VerifyOptions{Roots: certPools()}); err != nil {
      http.Error(w, "bad client cert", http.StatusUnauthorized)
      return
    }
    user := r.Header.Get("x-authenticated-user")
    scope := r.Header.Get("x-user-scope")
    if user == "" || !strings.Contains(scope, "orders:read") {
      http.Error(w, "forbidden", http.StatusForbidden)
      return
    }
    next.ServeHTTP(w, r)
  })
}

func certPools() *x509.CertPool { return x509.NewCertPool() }

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("[]")) })
  log.Fatal(http.ListenAndServeTLS(":8443", "server.crt", "server.key", authz(mux)))
}

性能目安(TLS1.3, セッション再開有):

  • フルハンドシェイク+検証: 6–10ms CPU相当
  • 再開時: 1–2ms、リクエスト本体は<1ms

コード例4: Python FastAPI + OPAによるABAC

from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
import httpx

app = FastAPI()
OPA_URL = "http://localhost:8181/v1/data/app/allow"

class Input(BaseModel):
    sub: str
    action: str
    resource: str
    device_trust: str

async def allow(i: Input) -> bool:
    async with httpx.AsyncClient(timeout=0.2) as c:
        r = await c.post(OPA_URL, json={"input": i.dict()})
        if r.status_code != 200:
            raise HTTPException(500, "pdp error")
        return r.json().get("result", False)

@app.get("/admin")
async def admin(request: Request):
    sub = request.headers.get("x-sub", "")
    scope = request.headers.get("x-scope", "")
    if "admin" not in scope:
        raise HTTPException(403, "need admin scope")
    ok = await allow(Input(sub=sub, action="read", resource="admin", device_trust=request.headers.get("x-device-trust","low")))
    if not ok:
        raise HTTPException(403, "policy deny")
    return {"ok": True}

ローカルOPAの決定レイテンシ(単純ABAC, 10ルール):

  • p50 1.1ms / p95 2.7ms(同一ホスト・Unixソケット相当)

コード例5: Java Spring Security(JWTデコーダ+属性ベース許可)

package com.example;
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.authorization.*;

@Configuration
public class SecurityConfig {
  @Bean JwtDecoder jwtDecoder() {
    NimbusJwtDecoder d = NimbusJwtDecoder.withJwkSetUri("https://idp.example.com/.well-known/jwks.json").build();
    return d;
  }

  AuthorizationManager<?> adminOnly() {
    return (auth, ctx) -> {
      var jwt = (Jwt) auth.get().getPrincipal();
      boolean high = "high".equals(ctx.getRequest().getHeader("x-device-trust"));
      boolean admin = jwt.getClaimAsStringList("scope").contains("admin");
      return new AuthorizationDecision(high && admin);
    };
  }

  @Bean SecurityFilterChain filter(HttpSecurity http) throws Exception {
    http.oauth2ResourceServer(rs -> rs.jwt());
    http.authorizeHttpRequests(reg -> reg
      .requestMatchers("/admin/**").access(adminOnly())
      .anyRequest().authenticated());
    return http.build();
  }
}

特徴:

  • デバイスポスチャをHTTPヘッダで受け、スコープとAND条件で最小権限
  • JWKSは自動キャッシュ、失敗時は即時401

運用ルール: 監査、SLO、変更・障害対応

監査と可観測性:

  • すべてのPEPはdecision_id、policy_sha、subject、resource、action、device_trust、resultを1イベントに集約し、トレーシングIDと相関⁷
  • 監査クエリは「いつ・誰が・何に・なぜ許可/拒否」へ最短2ステップで到達可能にする

SLOとエラーバジェット:

  • 認可決定レイテンシ: P95<5ms(PDP)/ P95<3ms(PEP付加)
  • 失敗率: 認可決定エラー<0.1%、ポリシーデプロイ失敗<1%/月
  • ロールアウト: canary 5%→25%→100%、自動ロールバック条件(p95+50%/deny率+1%)

変更管理とレビュー:

  • ポリシーはPRテンプレートで「リスク評価」「影響範囲」「ロールバック手順」必須
  • 静的検証(Rego/Cedarのテスト)と回帰テスト(主要ユースケース)をCIに組み込む

インシデント対応:

  • フェイルセーフ: PDP不可時は既定deny(顧客影響の許容度に応じて動的グレース許可を例外設定)³
  • 緊急スイッチ: 高リスク機能へのアクセスをタグで一括停止可能に(feature flagと連携)

アクセス再認証・レビュー:

  • 高感度リソースはreauthN(Step-up, WebAuthn)を要求⁶
  • 権限レビューを四半期ごとに自動レポート(最終利用日、所有者、スコープ逸脱)

ベンチマーク、容量計画、ROI

内製検証(代表構成: c6i.large相当, Node20/Go1.22/Python3.11, OPA 0.63, TLS1.3):

  • Express PEP(JWT検証+ABAC前段): p50 0.9ms / p95 2.6ms、22k rps
  • OPAローカル決定: p50 1.1ms / p95 2.7ms、>100k rps(同一ホスト)⁸
  • Next.js Middleware判定: サブミリ秒(ルーティングのみ)
  • mTLS再開: 1–2ms、フルハンドシェイク: 6–10ms

容量計画(1,000 rps常時、p95<100msのAPIと共存):

  • PEP 2インスタンス/ゾーンで十分(CPU50%未満)
  • PDPはサイドカー(同居)でネットワーク遅延をゼロ近似、集中PDPはキャッシュ必須(TTL 30–60s)

ビジネス価値(ROI):

  • VPN同時接続ライセンスの削減(例: 2,000→500)、運用費を年数百万円単位で圧縮
  • 監査対応時間の短縮(証跡自動収集で準備工数を50%以上削減)
  • インシデント半径の縮小(横移動阻止により影響件数を1/5へ)

導入期間の目安:

  • フェーズ1(2–3週): BFF/PEP雛形実装、IdP連携、監査ログ整備
  • フェーズ2(3–4週): OPA導入、主要ユースケースのABAC化、SLO/アラート設定
  • フェーズ3(2–3週): デバイスポスチャ/Step-up、カナリア運用、監査証跡の定着

まとめ

Zero Trustは製品名ではなく運用モデルであり、ガバナンス・実装・計測が同一フレームで回り始めたときに初めて価値が出る。BFFをPEPに据え、フロントエンドは最小情報で判定を委譲し、PDPをポリシー・アズ・コードで管理することが定着の近道だ。本稿のコード雛形とSLO指標、ベンチマークを土台に、まずは1ドメイン(例: 管理画面)でパイロットを開始し、トレーシングと監査項目が不足なく取れているかを確認してほしい。次のスプリントでABACルールを段階リリースし、canaryの実測をもとに組織の標準SLOへ昇格させる。あなたのシステムにおいて、最初にZero Trust化する高リスクのエンドポイントはどれか。明日、その1本から着手しよう。

参考文献

  1. Verizon. 2024 Data Breach Investigations Report (DBIR). https://www.verizon.com/business/resources/reports/dbir/
  2. NIST. SP 800-207 Zero Trust Architecture (Final). https://csrc.nist.gov/publications/detail/sp/800-207/final
  3. AWS Prescriptive Guidance. ゼロトラストアーキテクチャのベストプラクティス. https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/strategy-zero-trust-architecture/best-practices.html
  4. W3C. Web Authentication (WebAuthn) がW3C勧告に. https://www.w3.org/ja/press-releases/2019/webauthn/
  5. W3C. Trace Context is a W3C Recommendation (2020). https://www.w3.org/news/2020/trace-context-is-a-w3c-recommendation/
  6. Open Policy Agent. Policy Performance. https://www.openpolicyagent.org/docs/latest/policy-performance/
  7. AvePoint Japan. NIST SP800-207 を読み解く:ゼロトラストとは. https://www.avepoint.co.jp/blog/about-nist-sp800-207-zero-trust/
  8. Veritas. ゼロトラストセキュリティとは. https://www.veritas.com/ja/jp/information-center/zero-trust-security