LESSON 40分

ストーリー

佐藤CTO
SASTはコードを見る。でもコードだけ見ても分からない脆弱性がある

佐藤CTOがOWASP ZAPの画面を見せました。

佐藤CTO
DASTは実行中のアプリケーションに実際にリクエストを送って脆弱性を見つける。攻撃者と同じ視点だ
あなた
そしてコンテナセキュリティ。ベースイメージに脆弱性があれば、アプリケーションコードが完璧でも危険ですよね
佐藤CTO
その通り。ビルド成果物であるコンテナイメージもスキャンし、安全なものだけがデプロイされるようにする

DAST(Dynamic Application Security Testing)

DASTの仕組み

DASTは実行中のアプリケーションに対してHTTPリクエストを送信し、レスポンスを分析して脆弱性を検出します。

特徴SASTDAST
テスト方式ホワイトボックス(コード可視)ブラックボックス(外部から)
テスト対象ソースコード実行中のアプリケーション
検出対象コードの欠陥ランタイムの脆弱性、設定ミス
言語依存ありなし(HTTP通信のため)
False Positive多い比較的少ない
実行タイミングビルド前デプロイ後(テスト環境)

主要DASTツール

ツール特徴ライセンス
OWASP ZAP最も広く使われるOSSツールOSS
Burp Suiteプロ向けの包括的ツールCommercial
Nucleiテンプレートベースの高速スキャンOSS
NiktoWebサーバーの設定不備検出OSS

OWASP ZAP のCI/CD統合

# GitHub Actions での OWASP ZAP 自動スキャン
name: DAST Scan

on:
  deployment_status:
    # ステージング環境へのデプロイ完了後に実行

jobs:
  zap-scan:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: OWASP ZAP Full Scan
        uses: zaproxy/action-full-scan@v0.10.0
        with:
          target: ${{ github.event.deployment_status.target_url }}
          rules_file_name: .zap/rules.tsv
          cmd_options: >-
            -j
            -z "-config api.disablekey=true
                -config scanner.strength=HIGH
                -config scanner.threadPerHost=5"
          allow_issue_writing: true
          fail_action: true

      - name: Upload ZAP Report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap-report
          path: report_html.html
# .zap/rules.tsv -- アラートレベルのカスタマイズ
# ルールID	アクション(IGNORE, WARN, FAIL)
10016	IGNORE	# Web Browser XSS Protection Not Enabled(CSP で対応済み)
10021	WARN	# X-Content-Type-Options Header Missing
10038	FAIL	# Content Security Policy Header Not Set
40012	FAIL	# Cross Site Scripting (Reflected)
40014	FAIL	# Cross Site Scripting (Persistent)
90033	FAIL	# Loosely Scoped Cookie

APIセキュリティテスト

// OpenAPI仕様からの自動セキュリティテスト
interface ApiSecurityTest {
  endpoint: string;
  method: string;
  tests: SecurityTestCase[];
}

interface SecurityTestCase {
  name: string;
  category: string;
  payload: Record<string, any>;
  expectedStatus: number;
  description: string;
}

const apiSecurityTests: ApiSecurityTest[] = [
  {
    endpoint: '/api/users/:id',
    method: 'GET',
    tests: [
      {
        name: 'IDOR Test',
        category: 'Authorization',
        payload: { id: 'other-user-id' },
        expectedStatus: 403,
        description: '他ユーザーのIDでアクセスした場合に403を返すこと',
      },
      {
        name: 'SQL Injection',
        category: 'Injection',
        payload: { id: "1' OR '1'='1" },
        expectedStatus: 400,
        description: 'SQLi ペイロードが拒否されること',
      },
      {
        name: 'Path Traversal',
        category: 'Injection',
        payload: { id: '../../../etc/passwd' },
        expectedStatus: 400,
        description: 'パストラバーサルが拒否されること',
      },
    ],
  },
  {
    endpoint: '/api/auth/login',
    method: 'POST',
    tests: [
      {
        name: 'Rate Limiting',
        category: 'DoS',
        payload: { email: 'test@example.com', password: 'wrong' },
        expectedStatus: 429,
        description: '10回連続失敗後にレート制限されること',
      },
    ],
  },
];

コンテナセキュリティ

コンテナの攻撃面

graph TD
    Title["コンテナの攻撃面"]
    Title --> A["1. ベースイメージの脆弱性<br/>- OSパッケージの既知の脆弱性<br/>- 不要なパッケージの存在"]
    Title --> B["2. アプリケーション依存関係<br/>- npm/pipパッケージの脆弱性<br/>- 古いバージョンの使用"]
    Title --> C["3. Dockerfileの設定不備<br/>- rootユーザーでの実行<br/>- シークレットのレイヤーへの混入"]
    Title --> D["4. ランタイムの設定不備<br/>- 特権コンテナ<br/>- 過剰なケーパビリティ<br/>- ホストパスのマウント"]

    classDef title fill:#1a1a2e,stroke:#e94560,color:#fff
    classDef attack fill:#e94560,stroke:#c23050,color:#fff

    class Title title
    class A,B,C,D attack

セキュアなDockerfile

# ---- BAD: セキュリティの問題がある Dockerfile ----
# FROM node:18          # フルイメージ(脆弱性多)
# COPY . .              # 不要なファイルも含む
# RUN npm install       # devDependencies も含む
# EXPOSE 3000
# CMD ["node", "app.js"] # root で実行

# ---- GOOD: セキュアな Dockerfile ----
# Stage 1: ビルドステージ
FROM node:18-alpine AS builder
WORKDIR /app

# 依存関係のみ先にコピー(キャッシュ活用)
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY src/ ./src/
COPY tsconfig.json ./
RUN npm run build

# Stage 2: 実行ステージ
FROM gcr.io/distroless/nodejs18-debian12

# 非rootユーザー(distroless は nonroot ユーザーを内蔵)
USER nonroot:nonroot

WORKDIR /app

# ビルド成果物のみコピー
COPY --from=builder --chown=nonroot:nonroot /app/dist ./dist
COPY --from=builder --chown=nonroot:nonroot /app/node_modules ./node_modules

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD ["/nodejs/bin/node", "-e", "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"]

EXPOSE 3000

# 読み取り専用ファイルシステムで実行可能
CMD ["dist/app.js"]

Trivy によるコンテナスキャン

# GitHub Actions での Trivy スキャン
- name: Trivy vulnerability scan
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    format: table
    exit-code: 1
    ignore-unfixed: true
    vuln-type: os,library
    severity: CRITICAL,HIGH
    output: trivy-report.txt

# Trivy の設定ファイル
# trivy.yaml
severity:
  - CRITICAL
  - HIGH
ignore:
  unfixed: true
db:
  skip-update: false

Kubernetes Pod Security Standards

Pod Security Standards(PSS)の3レベル

レベル説明制限事項
Privileged制限なし開発環境向け
Baseline基本的な制限特権コンテナ禁止、HostPath禁止
Restricted最も厳格root実行禁止、読み取り専用FS推奨
# Namespace に Pod Security Standards を適用
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
---
# Restricted レベルに準拠した Pod 定義
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: production
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myapp:latest@sha256:abc123...  # ダイジェスト指定
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
      resources:
        limits:
          cpu: "500m"
          memory: "256Mi"
        requests:
          cpu: "250m"
          memory: "128Mi"
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir:
        sizeLimit: 100Mi

イメージの署名と検証

# Cosign でイメージに署名
# cosign sign --key cosign.key myregistry.com/myapp:latest

# Kubernetes での署名検証(Kyverno ポリシー)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "myregistry.com/myapp:*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----

まとめ

ポイント内容
DAST実行中のアプリケーションに対する動的テスト
OWASP ZAPCI/CDに統合可能なOSSのDASTツール
コンテナセキュリティベースイメージ、Dockerfile、ランタイム設定の多層防御
Trivyコンテナイメージの脆弱性スキャン
Pod Security StandardsK8sのPodセキュリティを3レベル(Privileged/Baseline/Restricted)で制御
イメージ署名Cosign + Kyverno で改ざん防止

チェックリスト

  • DASTとSASTの違いと使い分けを説明できる
  • OWASP ZAPをCI/CDパイプラインに統合できる
  • セキュアなDockerfileを記述できる
  • Trivyでコンテナイメージをスキャンできる
  • Kubernetes Pod Security Standardsを適用できる

次のステップへ

次は「IaCセキュリティ」を学びます。Terraform、CloudFormation等のIaCコードに対するセキュリティスキャンと、Policy as Codeの考え方を身につけましょう。


推定読了時間: 40分