# ADR-011: NetworkPolicy 變更治理架構 **狀態**: 提案 **日期**: 2026-03-23 **決策者**: 統帥 **觸發**: 多次 NetworkPolicy 變更導致生產事故 ## 問題陳述 ``` 事故時間線: ├── 2026-03-20: Worker 無法連 Redis → 發現 Egress 被阻擋 ├── 2026-03-22: OTEL 上報失敗 → 發現 Port 24317/24318 未開 ├── 2026-03-23: Y 按鈕執行超時 → K8s API 192.168.0.120:6443 未開 └── 每次都是「事後診斷」才發現 NetworkPolicy 問題 ``` **根本原因**: 1. 任何有 kubectl 權限的人都可以直接修改 NetworkPolicy 2. 修改後沒有任何告警或審計 3. Git 版本與叢集版本經常不同步 4. 沒有 Dry-Run / Diff 機制防止錯誤 --- ## 決策:三層防護架構 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ NetworkPolicy 變更治理架構 │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ Layer 1: GitOps (Single Source of Truth) │ │ ══════════════════════════════════════════ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 開發者 │ ──▶ │ PR 審核 │ ──▶ │ ArgoCD │ │ │ │ 修改 YAML │ │ 至少 2 人 │ │ 自動同步 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Git │ │ GitHub │ │ K8s │ │ │ │ Commit │ │ Actions │ │ Cluster │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ Layer 2: Policy Validation (防錯) │ │ ═══════════════════════════════════ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Kyverno / OPA Gatekeeper │ │ │ │ ──────────────────────────────── │ │ │ │ Rule 1: NetworkPolicy 必須有 system 標籤 │ │ │ │ Rule 2: 禁止 podSelector: {} (空選擇器) 覆蓋既有規則 │ │ │ │ Rule 3: Egress 必須明確指定 Port (禁止開放全部) │ │ │ │ Rule 4: 生產環境 NetworkPolicy 必須有註解說明 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ Layer 3: 變更告警 (監控) │ │ ════════════════════════════ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Falco / kube-state-metrics + Alertmanager │ │ │ │ ──────────────────────────────────────────── │ │ │ │ Alert: NetworkPolicy 被創建/修改/刪除 │ │ │ │ Alert: 直接 kubectl apply (繞過 GitOps) │ │ │ │ Alert: 非白名單用戶修改關鍵資源 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────┘ ``` --- ## 實施計畫 ### Phase 1: 立即措施 (Day 1) ✅ ```yaml # 1. 為所有 NetworkPolicy 加上變更元數據 metadata: annotations: awoooi.io/last-modified: "2026-03-23T15:30:00Z" awoooi.io/modified-by: "ogt" awoooi.io/change-reason: "修復 K8s API 連線 - Y 按鈕執行超時" awoooi.io/ticket: "AWOOOI-123" ``` ```bash # 2. 設定 kubectl 審計 (立即啟用) # 在 K3s server 加入: --audit-policy-file=/etc/rancher/k3s/audit-policy.yaml --audit-log-path=/var/log/k3s/audit.log ``` ### Phase 2: GitOps (Week 1) ```yaml # ArgoCD Application (輕量版) apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: awoooi-networkpolicy namespace: argocd spec: project: default source: repoURL: https://github.com/your-org/awoooi.git path: k8s/awoooi-prod targetRevision: main destination: server: https://kubernetes.default.svc namespace: awoooi-prod syncPolicy: automated: prune: false # 禁止自動刪除 selfHeal: true # 有人手動改會自動復原 syncOptions: - CreateNamespace=false ``` ### Phase 3: Policy Validation (Week 2) ```yaml # Kyverno Policy: 強制 NetworkPolicy 必須有註解 apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-networkpolicy-annotations spec: validationFailureAction: Enforce rules: - name: require-change-reason match: resources: kinds: - NetworkPolicy namespaces: - awoooi-prod validate: message: "NetworkPolicy 必須有 awoooi.io/change-reason 註解" pattern: metadata: annotations: awoooi.io/change-reason: "?*" awoooi.io/modified-by: "?*" ``` ### Phase 4: 變更告警 (Week 2) ```yaml # Falco Rule: NetworkPolicy 變更告警 - rule: NetworkPolicy Modified desc: 偵測 NetworkPolicy 被創建、修改或刪除 condition: > k8s_audit and ka.target.resource = "networkpolicies" and ka.verb in (create, update, patch, delete) output: > 🚨 NetworkPolicy 變更告警 [%ka.verb] %ka.target.namespace/%ka.target.name by %ka.user.name from %ka.sourceips priority: WARNING tags: [network, security, k8s] ``` ```yaml # Prometheus Alert: 直接 kubectl 修改 (繞過 GitOps) - alert: DirectKubectlNetworkPolicyChange expr: | increase(apiserver_audit_event_total{ verb=~"create|update|patch|delete", resource="networkpolicies", user_agent!~"argocd.*" }[5m]) > 0 for: 0m labels: severity: critical annotations: summary: "有人直接 kubectl 修改 NetworkPolicy (繞過 GitOps)" description: "{{ $labels.user }} 在 {{ $labels.namespace }} 修改了 NetworkPolicy" ``` --- ## PR 審核流程 (CODEOWNERS) ``` # .github/CODEOWNERS # NetworkPolicy 變更需要 CIO + SRE 雙重審核 k8s/*/02-network-policy.yaml @awoooi/cio @awoooi/sre-team k8s/*/networkpolicy*.yaml @awoooi/cio @awoooi/sre-team ``` --- ## 回滾機制 (統帥要求補充) ### 基線管理 ```bash k8s/awoooi-prod/ ├── 02-network-policy.yaml # 當前版本 ├── .baselines/ │ ├── LATEST_KNOWN_GOOD.yaml # 最後驗證通過版本 │ └── 02-network-policy.{date}.yaml # 歷史版本 ``` ### 一鍵回滾 ```bash # 緊急回滾 (< 30 秒) ./scripts/rollback-networkpolicy.sh awoooi-prod # 回滾到特定日期 ./scripts/rollback-networkpolicy.sh awoooi-prod 2026-03-22 ``` ### Drift 偵測 + 人工決策 (禁止盲目自動回滾) - 每 5 分鐘比對叢集狀態 vs Git 基線 - 偵測到差異 → 立即告警 + 顯示差異 - **人工判斷**: 合法變更 → 同步到 Git;錯誤變更 → 執行回滾 - 唯一自動回滾條件: 變更導致健康檢查失敗 + 變更在 10 分鐘內 + 基線曾健康 ### 回滾 vs 修復 決策樹 ``` 偵測到問題 │ ├─ 影響生產? │ ├─ 是 → 立即回滾 (< 30 秒) │ └─ 否 → 評估後決定 │ └─ 回滾後 ├─ 記錄 RCA ├─ 修復根因 └─ 更新基線 ``` --- ## 驗收標準 | 項目 | 狀態 | |------|------| | NetworkPolicy 只能透過 PR 修改 | ⬜ | | PR 需要 2 人審核 | ⬜ | | 直接 kubectl apply 會觸發告警 | ⬜ | | 告警發送到 Telegram + OpenClaw | ⬜ | | 審計日誌保留 90 天 | ⬜ | | Kyverno 強制註解規則 | ⬜ | | **基線快照每日保存** | ⬜ | | **一鍵回滾腳本可用** | ⬜ | | **Drift 偵測每 5 分鐘執行** | ⬜ | | **回滾耗時 < 30 秒** | ⬜ | --- ## 成本 | 元件 | 方案 | 成本 | |------|------|------| | GitOps | ArgoCD (已有) | $0 | | Policy | Kyverno (開源) | $0 | | 告警 | Falco + Alertmanager (開源) | $0 | | **總計** | | **$0** | --- ## 附錄 A: 2026-03-23 事故根因分析 ``` 2026-03-23 Y 按鈕執行超時 根因: NetworkPolicy allow-required-egress 遺漏 K8s API 實際端點 問題鏈: 1. ClusterIP 10.43.0.1:443 已允許 ✓ 2. 但實際流量路由到 192.168.0.120:6443 ✗ 3. 192.168.0.120 在 192.168.0.0/16 排除範圍內 → 被阻擋 修復: 新增 192.168.0.120:6443 到 allow-required-egress 教訓: 1. K8s Service ClusterIP ≠ 實際 Endpoint 2. NetworkPolicy 需要允許完整路由路徑 3. 變更前應該用 dry-run 驗證 ``` --- ## 附錄 B: 2026-03-26 DNS 規則事故根因分析 ``` 2026-03-26 兩天無 Telegram 告警 根因 1: Alertmanager URL 路徑錯誤 設定: /api/v1/webhook/alertmanager (單數) 實際: /api/v1/webhooks/alertmanager (複數) 結果: 404 Not Found 根因 2: NetworkPolicy DNS 規則標籤錯誤 設定: podSelector 要求 environment=prod, system=awoooi 實際: CoreDNS 只有 k8s-app=kube-dns 結果: Pod 無法連接 CoreDNS 根因 3: CoreDNS 上游 DNS 設定錯誤 設定: forward . /etc/resolv.conf → 127.0.0.53 (systemd-resolved) 實際: 容器內無法使用 127.0.0.53 結果: 外部 DNS 解析失敗 修復: 1. Alertmanager ConfigMap 修正 URL 路徑 2. NetworkPolicy 使用正確的 namespaceSelector 3. CoreDNS 改用 8.8.8.8 1.1.1.1 教訓: 1. URL 路徑必須經過 E2E 測試驗證 (ADR-025) 2. NetworkPolicy DNS 規則必須使用 namespace selector 3. CoreDNS 不能依賴宿主機的 systemd-resolved ``` ### DNS 規則最佳實踐 ```yaml # ✅ 正確的 DNS 規則寫法 - ports: - port: 53 protocol: UDP - port: 53 protocol: TCP to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ```