ストーリー
佐
佐藤CTO
セキュリティは後付けではなく、アーキテクチャの一部として最初から組み込むものだ
あなた
“Security by Design”ということですか?
あ
佐
佐藤CTO
その通り。そして、セキュリティはもはや技術だけの話じゃない。GDPRや個人情報保護法といった法規制への対応(コンプライアンス)も、設計段階で考慮する必要がある
佐
佐藤CTO
まさにそこがアーキテクトの仕事だ。今日はセキュリティアーキテクチャの全体像と、コンプライアンス対応の設計を学ぼう
セキュリティアーキテクチャの原則
多層防御(Defense in Depth)
単一の防御策に頼らず、複数の層で守ります。
graph TD
A["ネットワーク層<br/>WAF, DDoS防御, ファイアウォール"]
B["アプリケーション層<br/>認証, 認可, 入力検証, CSRF防御"]
C["データ層<br/>暗号化, アクセス制御, マスキング"]
D["インフラ層<br/>IAM, 監査ログ, セキュリティグループ"]
A --> B --> C --> D
classDef netStyle fill:#d9534f,stroke:#b52b27,color:#fff
classDef appStyle fill:#e8a838,stroke:#b07c1e,color:#fff
classDef dataStyle fill:#4a90d9,stroke:#2c5f8a,color:#fff
classDef infraStyle fill:#5cb85c,stroke:#3d8b3d,color:#fff
class A netStyle
class B appStyle
class C dataStyle
class D infraStyle
セキュリティ設計の7原則
| 原則 | 説明 | 実装例 |
|---|
| 最小権限 | 必要最小限のアクセス権のみ付与 | IAMロール、DBアクセス権限 |
| 多層防御 | 複数の防御層を設ける | WAF + 認証 + 暗号化 |
| フェイルセーフ | 障害時は安全側に倒す | 認証失敗時はアクセス拒否 |
| 攻撃面の最小化 | 露出するサービスを最小限に | 不要なポート閉鎖、API最小化 |
| 職務分離 | 権限を分散して不正を防止 | 承認者と実行者を分ける |
| 完全な仲介 | すべてのアクセスを検証 | API Gatewayでの認証チェック |
| 安全な既定値 | デフォルトを安全な状態に | デフォルト拒否ポリシー |
認証と認可
認証パターン
OAuth 2.0 / OpenID Connect(OIDC)
sequenceDiagram
participant Client
participant Auth as Auth Server
participant Resource as Resource Server
participant IdP
Client->>Auth: 1. 認可リクエスト
Auth->>IdP: 2. ログイン画面
IdP-->>Auth: 3. 認証完了
Auth-->>Client: 4. 認可コード
Client->>Auth: 5. トークン交換
Auth-->>Client: 6. Access Token + ID Token
Client->>Resource: 7. APIリクエスト(Bearer Token)
Resource-->>Client: 8. トークン検証 + レスポンス
// JWT検証ミドルウェアの実装例
import { verify, JwtPayload } from 'jsonwebtoken';
interface AuthenticatedRequest extends Request {
user: {
sub: string;
email: string;
roles: string[];
};
}
class JwtAuthMiddleware {
constructor(
private readonly publicKey: string,
private readonly issuer: string,
private readonly audience: string,
) {}
async authenticate(req: Request): Promise<AuthenticatedRequest> {
const authHeader = req.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedError('Missing or invalid Authorization header');
}
const token = authHeader.slice(7);
try {
const payload = verify(token, this.publicKey, {
algorithms: ['RS256'],
issuer: this.issuer,
audience: this.audience,
}) as JwtPayload;
return Object.assign(req, {
user: {
sub: payload.sub!,
email: payload.email as string,
roles: (payload.roles ?? []) as string[],
},
});
} catch (error) {
throw new UnauthorizedError('Invalid or expired token');
}
}
}
認可パターン
RBAC(Role-Based Access Control)
// ロールベースのアクセス制御
type Role = 'admin' | 'editor' | 'viewer';
type Permission = 'read' | 'write' | 'delete' | 'admin';
const rolePermissions: Record<Role, Permission[]> = {
admin: ['read', 'write', 'delete', 'admin'],
editor: ['read', 'write'],
viewer: ['read'],
};
class RBACAuthorizer {
authorize(userRoles: Role[], requiredPermission: Permission): boolean {
return userRoles.some((role) =>
rolePermissions[role]?.includes(requiredPermission)
);
}
}
// 使用例
class ArticleController {
constructor(private authorizer: RBACAuthorizer) {}
async deleteArticle(user: AuthenticatedUser, articleId: string): Promise<void> {
if (!this.authorizer.authorize(user.roles, 'delete')) {
throw new ForbiddenError('Insufficient permissions');
}
await this.articleService.delete(articleId);
}
}
ABAC(Attribute-Based Access Control)
// 属性ベースのアクセス制御
interface AccessPolicy {
effect: 'allow' | 'deny';
conditions: PolicyCondition[];
}
interface PolicyCondition {
attribute: string;
operator: 'equals' | 'contains' | 'greaterThan' | 'in';
value: unknown;
}
class ABACAuthorizer {
constructor(private policies: AccessPolicy[]) {}
authorize(context: AccessContext): boolean {
for (const policy of this.policies) {
const conditionsMet = policy.conditions.every((condition) =>
this.evaluateCondition(condition, context)
);
if (conditionsMet) {
return policy.effect === 'allow';
}
}
return false; // デフォルト拒否
}
private evaluateCondition(
condition: PolicyCondition,
context: AccessContext,
): boolean {
const actualValue = context[condition.attribute];
switch (condition.operator) {
case 'equals':
return actualValue === condition.value;
case 'contains':
return Array.isArray(actualValue) && actualValue.includes(condition.value);
case 'in':
return Array.isArray(condition.value) && condition.value.includes(actualValue);
default:
return false;
}
}
}
// ポリシー例:自部署のデータのみ編集可能
const policies: AccessPolicy[] = [
{
effect: 'allow',
conditions: [
{ attribute: 'action', operator: 'equals', value: 'edit' },
{ attribute: 'user.department', operator: 'equals', value: 'resource.department' },
],
},
];
RBACとABACの比較
| 観点 | RBAC | ABAC |
|---|
| 粒度 | ロール単位 | 属性の任意の組み合わせ |
| 柔軟性 | 低い(ロール爆発のリスク) | 高い(動的なポリシー) |
| 実装複雑度 | シンプル | 複雑 |
| 適用場面 | 組織の役割が明確な場合 | 細かい条件分岐が必要な場合 |
| 例 | 「管理者は全操作可能」 | 「勤務時間内かつ自部署データのみ編集可能」 |
APIセキュリティ
API セキュリティのベストプラクティス
// API セキュリティミドルウェアスタック
class APISecurityStack {
private middlewares: Middleware[] = [];
constructor() {
// 1. レートリミット
this.middlewares.push(new RateLimiter({
windowMs: 60_000, // 1分
maxRequests: 100, // 最大100リクエスト
keyExtractor: (req) => req.ip,
}));
// 2. CORS設定
this.middlewares.push(new CORSMiddleware({
allowedOrigins: ['https://app.example.com'],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
maxAge: 86400,
}));
// 3. 入力バリデーション
this.middlewares.push(new InputValidator({
maxBodySize: '1mb',
sanitizeHtml: true,
rejectSqlInjection: true,
}));
// 4. 認証
this.middlewares.push(new JwtAuthMiddleware(/* ... */));
// 5. 認可
this.middlewares.push(new RBACMiddleware(/* ... */));
// 6. 監査ログ
this.middlewares.push(new AuditLogger(/* ... */));
}
}
API脅威と対策
| 脅威 | 説明 | 対策 |
|---|
| インジェクション | SQL/NoSQL/コマンドインジェクション | パラメータバインディング、ORM使用 |
| 認証の不備 | 弱いパスワード、トークン管理の不備 | MFA、トークンローテーション |
| 過度なデータ公開 | 不要なフィールドの返却 | レスポンスフィルタリング |
| リソース制限の欠如 | 大量リクエスト、巨大ペイロード | レートリミット、サイズ制限 |
| 認可の不備 | 水平・垂直権限昇格 | オブジェクトレベルの認可チェック |
| Mass Assignment | 意図しないフィールドの更新 | DTO/ホワイトリスト方式 |
データ暗号化
保存時の暗号化(At Rest)
// フィールドレベル暗号化の例
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
class FieldEncryptor {
private readonly algorithm = 'aes-256-gcm';
constructor(private readonly masterKey: Buffer) {}
encrypt(plaintext: string): EncryptedField {
const iv = randomBytes(12);
const cipher = createCipheriv(this.algorithm, this.masterKey, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
return {
ciphertext: encrypted,
iv: iv.toString('base64'),
authTag: authTag.toString('base64'),
};
}
decrypt(field: EncryptedField): string {
const decipher = createDecipheriv(
this.algorithm,
this.masterKey,
Buffer.from(field.iv, 'base64'),
);
decipher.setAuthTag(Buffer.from(field.authTag, 'base64'));
let decrypted = decipher.update(field.ciphertext, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// 個人情報のフィールドレベル暗号化
interface UserRecord {
id: string;
email: EncryptedField; // 暗号化
phoneNumber: EncryptedField; // 暗号化
name: string; // 平文(検索に使用)
createdAt: Date;
}
通信時の暗号化(In Transit)
| 対象 | 方式 | 設定 |
|---|
| クライアント-サーバー間 | TLS 1.3 | HTTPS必須、HSTS設定 |
| サービス間通信 | mTLS | 双方向証明書検証 |
| データベース接続 | TLS | SSL/TLS接続を必須化 |
| メッセージキュー | TLS | ブローカーへの暗号化接続 |
コンプライアンス要件
主要な法規制・基準
| 規制/基準 | 対象 | 主な要件 |
|---|
| GDPR | EU市民のデータを扱うサービス | データ主体の権利、DPO設置、72時間以内の侵害通知 |
| 個人情報保護法 | 日本国内の個人データ | 利用目的の特定、安全管理措置、第三者提供制限 |
| PCI-DSS | クレジットカード情報を扱うシステム | カードデータの暗号化、アクセス制限、定期監査 |
| SOC 2 | SaaSサービス提供者 | セキュリティ、可用性、処理の完全性、機密性、プライバシー |
| HIPAA | 医療情報を扱うシステム(米国) | PHI(保護対象医療情報)の保護 |
GDPR対応のアーキテクチャ考慮事項
// GDPRの「忘れられる権利」への対応
interface DataSubjectRightsHandler {
// データアクセス権:ユーザーが自分のデータを閲覧
handleAccessRequest(userId: string): Promise<PersonalDataExport>;
// データ削除権(忘れられる権利)
handleErasureRequest(userId: string): Promise<ErasureResult>;
// データポータビリティ権:他サービスへのデータ移行
handlePortabilityRequest(userId: string): Promise<PortableDataPackage>;
// 処理制限権
handleRestrictionRequest(userId: string): Promise<void>;
}
class GDPRCompliantUserService implements DataSubjectRightsHandler {
async handleErasureRequest(userId: string): Promise<ErasureResult> {
const result: ErasureResult = { deletedSystems: [], errors: [] };
// 1. メインDBからの削除
await this.userRepository.delete(userId);
result.deletedSystems.push('main-database');
// 2. 検索インデックスからの削除
await this.searchIndex.deleteUser(userId);
result.deletedSystems.push('search-index');
// 3. キャッシュの無効化
await this.cache.invalidateUser(userId);
result.deletedSystems.push('cache');
// 4. バックアップからの削除(暗号化キーの破棄で対応)
await this.encryptionKeyManager.revokeUserKey(userId);
result.deletedSystems.push('backup-encryption-keys');
// 5. 監査ログは法的保持義務があるため匿名化
await this.auditLog.anonymize(userId);
result.deletedSystems.push('audit-log-anonymized');
// 6. 削除完了の記録
await this.erasureLog.record({
userId,
requestedAt: new Date(),
completedAt: new Date(),
systems: result.deletedSystems,
});
return result;
}
}
脅威モデリング(STRIDE)
STRIDEフレームワーク
| カテゴリ | 脅威 | 対策 |
|---|
| Spoofing(なりすまし) | 他人になりすまして操作 | 強力な認証(MFA) |
| Tampering(改ざん) | データやコードの改ざん | デジタル署名、整合性チェック |
| Repudiation(否認) | 行為の否認 | 監査ログ、タイムスタンプ |
| Information Disclosure(情報漏洩) | 機密情報の漏洩 | 暗号化、アクセス制御 |
| Denial of Service(サービス拒否) | サービスの停止 | レートリミット、スケーリング |
| Elevation of Privilege(権限昇格) | 権限を超えた操作 | 最小権限、認可チェック |
脅威モデリングの実施例
// 脅威モデリングテンプレート
interface ThreatModel {
system: string;
dataFlows: DataFlow[];
threats: Threat[];
mitigations: Mitigation[];
}
interface Threat {
id: string;
category: 'S' | 'T' | 'R' | 'I' | 'D' | 'E';
description: string;
affectedComponent: string;
riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
likelihood: 'HIGH' | 'MEDIUM' | 'LOW';
impact: 'HIGH' | 'MEDIUM' | 'LOW';
}
// ECサイトの脅威モデリング例
const ecommerceThreatModel: ThreatModel = {
system: 'ECサイト',
dataFlows: [
{ from: 'ユーザー', to: 'API Gateway', data: '認証情報' },
{ from: 'API Gateway', to: '注文サービス', data: '注文データ' },
{ from: '注文サービス', to: '決済サービス', data: 'カード情報' },
],
threats: [
{
id: 'T-001',
category: 'S',
description: 'セッションハイジャックによるなりすまし注文',
affectedComponent: 'API Gateway',
riskLevel: 'CRITICAL',
likelihood: 'MEDIUM',
impact: 'HIGH',
},
{
id: 'T-002',
category: 'I',
description: 'APIレスポンスからのカード情報漏洩',
affectedComponent: '決済サービス',
riskLevel: 'CRITICAL',
likelihood: 'LOW',
impact: 'HIGH',
},
{
id: 'T-003',
category: 'D',
description: 'セール時のDDoS攻撃によるサービス停止',
affectedComponent: 'API Gateway',
riskLevel: 'HIGH',
likelihood: 'HIGH',
impact: 'HIGH',
},
],
mitigations: [
{ threatId: 'T-001', measure: 'HttpOnly/Secure Cookie + CSRFトークン + セッション固定化対策' },
{ threatId: 'T-002', measure: 'カード情報のトークン化(PCI-DSS準拠の外部決済サービス利用)' },
{ threatId: 'T-003', measure: 'WAF + CDN + オートスケーリング + レートリミット' },
],
};
セキュリティアーキテクチャレビューチェックリスト
レビュー項目
## 認証・認可
- [ ] 認証方式は適切か(OAuth2/OIDC、MFA対応)
- [ ] パスワードはbcrypt/argon2でハッシュ化されているか
- [ ] JWTの署名アルゴリズムはRS256以上か
- [ ] トークンの有効期限は適切か(アクセス15分、リフレッシュ7日以内)
- [ ] 認可はリソースレベルで実装されているか
## データ保護
- [ ] 個人情報は暗号化されているか(保存時・通信時)
- [ ] 暗号鍵の管理はKMSを使用しているか
- [ ] バックアップデータも暗号化されているか
- [ ] ログに個人情報が出力されていないか
## API セキュリティ
- [ ] HTTPS が必須になっているか
- [ ] レートリミットが設定されているか
- [ ] 入力バリデーションが全エンドポイントで実装されているか
- [ ] CORS設定は最小限か
## インフラストラクチャ
- [ ] セキュリティグループは最小権限か
- [ ] 不要なポートは閉じているか
- [ ] IAMロールは最小権限か
- [ ] シークレット(API キー、パスワード)はSecrets Managerに格納されているか
## 監視・検知
- [ ] セキュリティイベントの監視は設定されているか
- [ ] 不正アクセスの検知とアラートは設定されているか
- [ ] 監査ログは改ざん防止されているか
- [ ] インシデント対応手順は文書化されているか
## コンプライアンス
- [ ] 対象となる法規制を特定しているか
- [ ] データの保持期間ポリシーは定義されているか
- [ ] データ主体の権利行使に対応できるか
- [ ] プライバシーポリシーは更新されているか
まとめ
| ポイント | 内容 |
|---|
| Security by Design | セキュリティはアーキテクチャ設計の初期段階から組み込む |
| 多層防御 | ネットワーク、アプリケーション、データ、インフラの各層で防御 |
| 認証・認可 | OAuth2/OIDC + RBAC/ABACの適切な選択 |
| データ暗号化 | 保存時(At Rest)と通信時(In Transit)の両方を暗号化 |
| コンプライアンス | 対象法規制を特定し、アーキテクチャで対応 |
| 脅威モデリング | STRIDEで体系的に脅威を特定し、対策を設計 |
チェックリスト
次のステップへ
セキュリティとコンプライアンスの設計を学びました。次は総合演習です。ここまで学んだ非機能要件の知識を使い、実際のシステムに対してNFRを定義する演習に取り組みましょう。
推定読了時間: 30分