diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index fb30f062a..2d768cdea 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -14864,7 +14864,7 @@ }, "coverage": { "label": "平均成熟度", - "detail": "64% 只代表只讀框架成熟度。" + "detail": "65% 只代表只讀框架成熟度。" }, "runtimeGate": { "label": "執行期", @@ -14878,7 +14878,7 @@ }, "sshNetwork": { "title": "SSH / network / firewall", - "body": "下一步補 target whitelist、host key policy、ingress / egress matrix、network owner 與維護窗口。" + "body": "repo-only 清冊已納入 16 個 SSH / network access surface;下一步仍需 live firewall / sudoers / known_hosts / NetworkPolicy / NodePort / WireGuard evidence、network owner 與 rollback owner。" }, "backupRestore": { "title": "Backup / restore / escrow", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index fb30f062a..2d768cdea 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -14864,7 +14864,7 @@ }, "coverage": { "label": "平均成熟度", - "detail": "64% 只代表只讀框架成熟度。" + "detail": "65% 只代表只讀框架成熟度。" }, "runtimeGate": { "label": "執行期", @@ -14878,7 +14878,7 @@ }, "sshNetwork": { "title": "SSH / network / firewall", - "body": "下一步補 target whitelist、host key policy、ingress / egress matrix、network owner 與維護窗口。" + "body": "repo-only 清冊已納入 16 個 SSH / network access surface;下一步仍需 live firewall / sudoers / known_hosts / NetworkPolicy / NodePort / WireGuard evidence、network owner 與 rollback owner。" }, "backupRestore": { "title": "Backup / restore / escrow", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 9259de6f8..3428d5c9c 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -2071,13 +2071,13 @@ const highValueConfigOwnerPacketSummary = [ const highValueConfigControlCoverageSummary = [ { key: 'categories', value: '14', icon: ListChecks, tone: 'steady' }, { key: 'c0', value: '8', icon: AlertTriangle, tone: 'warn' }, - { key: 'coverage', value: '64%', icon: ShieldCheck, tone: 'warn' }, + { key: 'coverage', value: '65%', icon: ShieldCheck, tone: 'warn' }, { key: 'runtimeGate', value: '0', icon: Lock, tone: 'locked' }, ] as const const highValueConfigControlCoverageItems: HighValueConfigControlCoverageItem[] = [ { key: 'dockerSystemd', rank: 'P1-1', value: '50%', icon: Server, tone: 'warn' }, - { key: 'sshNetwork', rank: 'P1-2', value: '48%', icon: Network, tone: 'warn' }, + { key: 'sshNetwork', rank: 'P1-2', value: '54%', icon: Network, tone: 'warn' }, { key: 'backupRestore', rank: 'P1-3', value: '52%', icon: Database, tone: 'warn' }, { key: 'monitoring', rank: 'P1-4', value: '56%', icon: Radar, tone: 'warn' }, ] @@ -2088,7 +2088,7 @@ const highValueConfigControlCoverageBoundaries = [ 'high_value_config_control_coverage_category_count=14', 'high_value_config_control_coverage_c0_category_count=8', 'high_value_config_control_coverage_c1_category_count=4', - 'high_value_config_control_coverage_average_percent=64', + 'high_value_config_control_coverage_average_percent=65', 'high_value_config_control_coverage_needs_live_evidence_count=6', 'high_value_config_control_coverage_owner_response_required_count=14', 'high_value_config_control_coverage_owner_response_received_count=0', @@ -2098,10 +2098,22 @@ const highValueConfigControlCoverageBoundaries = [ 'host_service_config_inventory_surface_count=9', 'host_service_config_inventory_write_capable_surface_count=3', 'host_service_config_inventory_runtime_gate_count=0', + 'ssh_network_access_inventory_surface_count=16', + 'ssh_network_access_inventory_write_capable_surface_count=6', + 'ssh_network_access_inventory_runtime_gate_count=0', 'docker_compose_action_authorized=false', 'systemctl_action_authorized=false', 'repair_bot_execution_authorized=false', 'ansible_apply_authorized=false', + 'ssh_read_authorized=false', + 'ssh_write_authorized=false', + 'sudo_action_authorized=false', + 'firewall_change_authorized=false', + 'network_policy_apply_authorized=false', + 'nodeport_change_authorized=false', + 'wireguard_change_authorized=false', + 'known_hosts_patch_authorized=false', + 'host_keyscan_authorized=false', 'runtime_execution_authorized=false', 'host_write_authorized=false', 'nginx_reload_authorized=false', diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 7cb321934..647a0f8b5 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -28,6 +28,46 @@ - IwoooS 整體仍維持 `64%`;active runtime gate 仍 `0`。 **邊界**:本段只做只讀批准包、snapshot、schema、API、測試、前端可視化與文件;未寫 Gateway queue、未呼叫 Telegram Bot API、未改 receiver route、未寫 delivery receipt、未啟動 retry worker、未發 Telegram、未 SSH、未 active scan、未收 secret value、未開 runtime gate、未建立任何 P2-403E 內容區執行按鈕。 +## 2026-06-11|IwoooS P1-2 SSH / network access repo-only 清冊 + +**背景**:統帥要求所有重要配置都要被資安控管,尤其 Nginx 以外的 SSH、sudoers、known_hosts、firewall、WireGuard、NetworkPolicy 與 NodePort 不能只靠分散文件記憶。本段延續「先建立框架、只讀證據、低摩擦流程,再階段性收攏」原則,只做 repo-only 清冊,不連主機、不掃描、不改 live network。 + +**完成**: + +- 新增 `ssh_network_access_inventory_v1` 產生器、schema、snapshot 與人讀文件,納入 `16` 個 repo-only surface。 +- 清冊覆蓋 SSH target、known_hosts workflow、CI deploy SSH、monitoring SSH、backup SSH capture、sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 alert SSH action catalog。 +- SSH source surface `11`、NetworkPolicy surface `2`、NodePort surface `2`、sudoers surface `1`、WireGuard surface `1`、write-capable surface `6`。 +- 所有 surface 均固定 owner response required 與 live evidence required;owner response received / accepted、live evidence、maintenance window、rollback owner、runtime gate 與 action button 全部仍為 `0`。 +- 高價值配置覆蓋矩陣的 `ssh_firewall_network_access` 從 `48%` 推進到 `54%`;高價值配置平均只讀成熟度從 `64%` 調整為 `65%`。這只代表 repo-only 清冊完成,不代表主機、firewall 或 WireGuard 已驗證或可執行。 +- IwoooS posture projection snapshot / schema、`security-mirror-progress-guard.py`、高價值配置文件、配置控管總清冊與前端高價值配置卡已同步新口徑。 +- `/zh-TW/iwooos` 的 P1-2 卡由 `48%` 更新為 `54%`,並新增 `ssh_network_access_inventory_surface_count=16`、`ssh_network_access_inventory_write_capable_surface_count=6`、`ssh_read_authorized=false`、`sudo_action_authorized=false`、`firewall_change_authorized=false`、`network_policy_apply_authorized=false`、`nodeport_change_authorized=false`、`wireguard_change_authorized=false`、`known_hosts_patch_authorized=false`、`host_keyscan_authorized=false` 等前台邊界標記;仍無任何操作按鈕。 + +**本地驗證**: + +- `python3 scripts/security/ssh-network-access-inventory.py --root . --output /tmp/ssh-network-access-inventory-check.json`:`SSH_NETWORK_ACCESS_INVENTORY_OK surfaces=16 ssh_sources=11 nodeports=2 runtime_gate=0`。 +- `python3 scripts/security/high-value-config-control-coverage.py --root . --output /tmp/high-value-config-control-coverage-check.json`:`HIGH_VALUE_CONFIG_CONTROL_COVERAGE_OK categories=14 c0=8 avg=65 runtime_gate=0`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `python3 scripts/security/source-control-owner-response-guard.py --root .`:`SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`。 +- `python3 -m py_compile scripts/security/ssh-network-access-inventory.py scripts/security/high-value-config-control-coverage.py scripts/security/security-mirror-progress-guard.py`:通過。 +- JSON parse:SSH / network snapshot / schema、高價值覆蓋 snapshot、IwoooS posture projection snapshot / schema、`zh-TW.json`、`en.json` 通過。 +- `cmp -s apps/web/messages/zh-TW.json apps/web/messages/en.json`:通過。 +- `python3 scripts/ops/doc-secrets-sanity-check.py docs .gitea`:`DOC_SECRET_SANITY_OK scanned_files=684`。 +- `pnpm --filter @awoooi/web typecheck`:通過。暫存 worktree 原本缺 `node_modules`,已用 lockfile 安裝本地依賴後驗證。 +- `git diff --check`:通過。 + +**完成度同步**: + +- P1-2 repo-only surface 註冊:`100%`。 +- source existence / SHA256 hash:`100%`。 +- SSH / network 高價值配置成熟度:`48% -> 54%`。 +- owner response 收件 / 接受:`0%`。 +- live evidence collection:`0%`。 +- SSH / sudo / firewall / NetworkPolicy / NodePort / WireGuard / known_hosts gate:`0%`。 +- IwoooS 整體仍維持 `64%`;active runtime gate 仍 `0`。 + +**待正式站驗證**:推送並等待 Gitea CD 完成後,需以 production `/zh-TW/iwooos` 做 desktop / mobile sanity,確認 P1-2 卡顯示 `54%`、新 boundary markers 可見、卡內操作元素 `0`、無水平溢出,並補 deploy marker、Gitea run 與截圖路徑。 + +**邊界**:本段未 SSH、未 keyscan、未讀 live host、未讀 live firewall、未讀 live sudoers、未套用 NetworkPolicy、未改 NodePort、未啟動 WireGuard、未執行 sudo、未 active scan、未收 secret value、未改主機與未新增任何前端執行按鈕。 ## 2026-06-11|IwoooS P1-1 Docker / systemd repo-only 清冊 diff --git a/docs/schemas/iwooos_posture_projection_v1.schema.json b/docs/schemas/iwooos_posture_projection_v1.schema.json index 7089df2f9..f0d9a84bf 100644 --- a/docs/schemas/iwooos_posture_projection_v1.schema.json +++ b/docs/schemas/iwooos_posture_projection_v1.schema.json @@ -195,6 +195,27 @@ "host_service_config_inventory_action_button_count", "host_service_config_inventory_coverage_percent_before_inventory", "host_service_config_inventory_coverage_percent_after_inventory", + "ssh_network_access_inventory_first_layer", + "ssh_network_access_inventory_surface_count", + "ssh_network_access_inventory_source_exists_count", + "ssh_network_access_inventory_expected_scope_count", + "ssh_network_access_inventory_ssh_source_surface_count", + "ssh_network_access_inventory_network_policy_surface_count", + "ssh_network_access_inventory_nodeport_surface_count", + "ssh_network_access_inventory_sudoers_surface_count", + "ssh_network_access_inventory_wireguard_surface_count", + "ssh_network_access_inventory_write_capable_surface_count", + "ssh_network_access_inventory_owner_response_required_count", + "ssh_network_access_inventory_owner_response_received_count", + "ssh_network_access_inventory_owner_response_accepted_count", + "ssh_network_access_inventory_live_evidence_required_count", + "ssh_network_access_inventory_live_evidence_received_count", + "ssh_network_access_inventory_maintenance_window_accepted_count", + "ssh_network_access_inventory_rollback_owner_accepted_count", + "ssh_network_access_inventory_runtime_gate_count", + "ssh_network_access_inventory_action_button_count", + "ssh_network_access_inventory_coverage_percent_before_inventory", + "ssh_network_access_inventory_coverage_percent_after_inventory", "high_value_config_owner_packet_first_layer", "high_value_config_owner_packet_summary_count", "high_value_config_owner_packet_item_count", @@ -513,7 +534,7 @@ }, "high_value_config_control_coverage_average_percent": { "type": "integer", - "const": 64 + "const": 65 }, "high_value_config_control_coverage_needs_live_evidence_count": { "type": "integer", @@ -850,6 +871,90 @@ "s4_9_owner_response_metadata_intake_action_buttons_allowed": { "type": "boolean", "const": false + }, + "ssh_network_access_inventory_first_layer": { + "type": "boolean", + "const": true + }, + "ssh_network_access_inventory_surface_count": { + "type": "integer", + "const": 16 + }, + "ssh_network_access_inventory_source_exists_count": { + "type": "integer", + "const": 16 + }, + "ssh_network_access_inventory_expected_scope_count": { + "type": "integer", + "const": 16 + }, + "ssh_network_access_inventory_ssh_source_surface_count": { + "type": "integer", + "const": 11 + }, + "ssh_network_access_inventory_network_policy_surface_count": { + "type": "integer", + "const": 2 + }, + "ssh_network_access_inventory_nodeport_surface_count": { + "type": "integer", + "const": 2 + }, + "ssh_network_access_inventory_sudoers_surface_count": { + "type": "integer", + "const": 1 + }, + "ssh_network_access_inventory_wireguard_surface_count": { + "type": "integer", + "const": 1 + }, + "ssh_network_access_inventory_write_capable_surface_count": { + "type": "integer", + "const": 6 + }, + "ssh_network_access_inventory_owner_response_required_count": { + "type": "integer", + "const": 16 + }, + "ssh_network_access_inventory_owner_response_received_count": { + "type": "integer", + "const": 0 + }, + "ssh_network_access_inventory_owner_response_accepted_count": { + "type": "integer", + "const": 0 + }, + "ssh_network_access_inventory_live_evidence_required_count": { + "type": "integer", + "const": 16 + }, + "ssh_network_access_inventory_live_evidence_received_count": { + "type": "integer", + "const": 0 + }, + "ssh_network_access_inventory_maintenance_window_accepted_count": { + "type": "integer", + "const": 0 + }, + "ssh_network_access_inventory_rollback_owner_accepted_count": { + "type": "integer", + "const": 0 + }, + "ssh_network_access_inventory_runtime_gate_count": { + "type": "integer", + "const": 0 + }, + "ssh_network_access_inventory_action_button_count": { + "type": "integer", + "const": 0 + }, + "ssh_network_access_inventory_coverage_percent_before_inventory": { + "type": "integer", + "const": 48 + }, + "ssh_network_access_inventory_coverage_percent_after_inventory": { + "type": "integer", + "const": 54 } }, "additionalProperties": false diff --git a/docs/schemas/ssh_network_access_inventory_v1.schema.json b/docs/schemas/ssh_network_access_inventory_v1.schema.json new file mode 100644 index 000000000..6edc37d7c --- /dev/null +++ b/docs/schemas/ssh_network_access_inventory_v1.schema.json @@ -0,0 +1,211 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.wooo.work/schemas/ssh_network_access_inventory_v1.schema.json", + "title": "IwoooS SSH / network access repo-only 清冊", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "generated_at", + "git_commit", + "status", + "source_scope", + "summary", + "execution_boundaries", + "expected_scopes", + "access_surfaces", + "write_capable_surfaces", + "next_collection_order", + "operator_interpretation" + ], + "properties": { + "schema_version": { + "const": "ssh_network_access_inventory_v1" + }, + "generated_at": { + "type": "string" + }, + "git_commit": { + "type": "string" + }, + "status": { + "const": "repo_only_inventory_ready" + }, + "source_scope": { + "const": "committed_repo_files_only" + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "surface_count", + "source_exists_count", + "expected_scope_count", + "ssh_source_surface_count", + "network_policy_surface_count", + "nodeport_surface_count", + "sudoers_surface_count", + "wireguard_surface_count", + "write_capable_surface_count", + "surfaces_requiring_owner_response_count", + "surfaces_requiring_live_evidence_count", + "owner_response_received_count", + "owner_response_accepted_count", + "live_evidence_received_count", + "maintenance_window_accepted_count", + "rollback_owner_accepted_count", + "runtime_gate_count", + "action_button_count", + "coverage_percent_after_inventory", + "coverage_percent_before_inventory" + ], + "properties": { + "surface_count": { "const": 16 }, + "source_exists_count": { "const": 16 }, + "expected_scope_count": { "const": 16 }, + "ssh_source_surface_count": { "const": 11 }, + "network_policy_surface_count": { "const": 2 }, + "nodeport_surface_count": { "const": 2 }, + "sudoers_surface_count": { "const": 1 }, + "wireguard_surface_count": { "const": 1 }, + "write_capable_surface_count": { "const": 6 }, + "surfaces_requiring_owner_response_count": { "const": 16 }, + "surfaces_requiring_live_evidence_count": { "const": 16 }, + "owner_response_received_count": { "const": 0 }, + "owner_response_accepted_count": { "const": 0 }, + "live_evidence_received_count": { "const": 0 }, + "maintenance_window_accepted_count": { "const": 0 }, + "rollback_owner_accepted_count": { "const": 0 }, + "runtime_gate_count": { "const": 0 }, + "action_button_count": { "const": 0 }, + "coverage_percent_after_inventory": { "const": 54 }, + "coverage_percent_before_inventory": { "const": 48 } + } + }, + "execution_boundaries": { + "type": "object", + "additionalProperties": { "const": false }, + "required": [ + "runtime_execution_authorized", + "host_write_authorized", + "ssh_read_authorized", + "ssh_write_authorized", + "sudo_action_authorized", + "firewall_change_authorized", + "network_policy_apply_authorized", + "nodeport_change_authorized", + "wireguard_change_authorized", + "known_hosts_patch_authorized", + "host_keyscan_authorized", + "live_host_read_authorized", + "secret_value_collection_allowed", + "ssh_key_collection_allowed", + "active_scan_authorized", + "action_buttons_allowed" + ] + }, + "expected_scopes": { + "type": "array", + "minItems": 16, + "maxItems": 16, + "items": { "type": "string" } + }, + "access_surfaces": { + "type": "array", + "minItems": 16, + "maxItems": 16, + "items": { + "$ref": "#/$defs/access_surface" + } + }, + "write_capable_surfaces": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "surface_id", + "label", + "config_kind", + "expected_scope", + "required_gate" + ], + "properties": { + "surface_id": { "type": "string" }, + "label": { "type": "string" }, + "config_kind": { "type": "string" }, + "expected_scope": { "type": "string" }, + "required_gate": { + "const": "owner_response_plus_maintenance_window_plus_rollback_owner" + } + } + } + }, + "next_collection_order": { + "type": "array", + "minItems": 10, + "items": { "type": "string" } + }, + "operator_interpretation": { + "type": "array", + "items": { "type": "string" } + } + }, + "$defs": { + "access_surface": { + "type": "object", + "additionalProperties": false, + "required": [ + "surface_id", + "label", + "source_path", + "expected_scope", + "config_kind", + "control_tier", + "current_state", + "access_scope", + "requires_live_evidence", + "requires_owner_response", + "next_owner_action", + "source_exists", + "line_count", + "sha256", + "owner_response_received", + "owner_response_accepted", + "live_evidence_received", + "maintenance_window_accepted", + "rollback_owner_accepted", + "runtime_gate_open", + "action_buttons_allowed" + ], + "properties": { + "surface_id": { "type": "string" }, + "label": { "type": "string" }, + "source_path": { "type": "string" }, + "expected_scope": { "type": "string" }, + "config_kind": { "type": "string" }, + "control_tier": { "const": "C1" }, + "current_state": { "type": "string" }, + "access_scope": { + "type": "array", + "items": { "type": "string" } + }, + "requires_live_evidence": { "const": true }, + "requires_owner_response": { "const": true }, + "next_owner_action": { "type": "string" }, + "source_exists": { "const": true }, + "line_count": { "type": "integer", "minimum": 1 }, + "sha256": { "type": "string", "minLength": 64, "maxLength": 64 }, + "owner_response_received": { "const": false }, + "owner_response_accepted": { "const": false }, + "live_evidence_received": { "const": false }, + "maintenance_window_accepted": { "const": false }, + "rollback_owner_accepted": { "const": false }, + "runtime_gate_open": { "const": false }, + "action_buttons_allowed": { "const": false } + } + } + } +} diff --git a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md index d012eacd1..2854a3945 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md @@ -24,7 +24,7 @@ | C1 類別 | `4` | 監控、Docker / systemd、SSH / network、AI provider | | C2 類別 | `1` | 產品 runtime route 與跨產品邊界 | | C3 類別 | `1` | security evidence / snapshot / guard tooling | -| 平均只讀控管成熟度 | `64%` | 僅代表框架 / evidence / owner packet 準備度,不代表 runtime 可執行 | +| 平均只讀控管成熟度 | `65%` | 僅代表框架 / evidence / owner packet 準備度,不代表 runtime 可執行 | | 需要 live evidence 的類別 | `6` | 只能等 owner-provided redacted evidence 或維護窗口,不主動修改主機 | | owner response required | `14` | 每類都需要 owner response 才能往 accepted 前進 | | owner response received / accepted | `0 / 0` | 不得假性提高 | @@ -35,7 +35,7 @@ | 優先 | 類別 | 目前成熟度 | 下一步 | |------|------|------------|--------| | P1-1 | Docker Compose / systemd / host service config | `50%` | repo-only 清冊已納入 9 個 surface;仍缺 110 / 188 live hash、restart window、rollback owner 與 post-check 指標 | -| P1-2 | SSH / sudoers / known_hosts / firewall / WireGuard / NodePort | `48%` | 補 target whitelist、host key policy、ingress / egress matrix、network owner 與 rollback owner | +| P1-2 | SSH / sudoers / known_hosts / firewall / WireGuard / NodePort | `54%` | repo-only 清冊已納入 16 個 SSH / network access surface;仍缺 live evidence、owner 與 rollback | | P1-3 | Backup / restore / escrow / retention | `52%` | 補 restore drill approval package、offsite escrow owner、retention owner 與 no-secret-value evidence | | P1-4 | Prometheus / Alertmanager / Grafana / SigNoz / Sentry / Langfuse | `56%` | 補 rule diff、receiver diff、reload owner、failure-only notification policy 與 route smoke | @@ -100,3 +100,7 @@ python3 scripts/security/high-value-config-control-coverage.py \ ## 8. P1-1 Docker / systemd 清冊更新 `host_service_config_inventory_v1` 已把 Docker Compose、systemd / repair-bot、Ansible service role 與 host config backup capture 納入 repo-only 清冊,共 `9` 個 surface、`3` 個 write-capable surface、`2` 個 repair-bot whitelist、`1` 個 systemd restart surface。此更新只讓 `docker_compose_systemd_host_config` 從 `42%` 推進到 `50%`;owner response、live evidence、restart window、rollback owner、runtime gate 與 action button 仍全部為 `0`。 + +## 9. P1-2 SSH / network access 清冊更新 + +`ssh_network_access_inventory_v1` 已把 SSH target、known_hosts workflow、CI deploy SSH、monitoring SSH、backup SSH capture、sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 alert SSH action catalog 納入 repo-only 清冊,共 `16` 個 surface、`6` 個 write-capable surface、`2` 個 NetworkPolicy、`2` 個 NodePort、`1` 個 sudoers surface 與 `1` 個 WireGuard surface。此更新只讓 `ssh_firewall_network_access` 從 `48%` 推進到 `54%`;owner response、live evidence、maintenance window、rollback owner、runtime gate 與 action button 仍全部為 `0`。 diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index 32e4b206d..b9a3add8b 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -32,7 +32,7 @@ | 註冊配置類別 | `14` | 代表已進 Gate 分類,不代表已批准 | | C0 類別 | `8` | Nginx、DNS / TLS、K8s、secret、workflow / runner、runtime config、backup、agent-bounty runtime | | C1 類別 | `4` | 監控、Docker / systemd、SSH / network、AI provider | -| 平均只讀控管成熟度 | `64%` | 只代表框架 / evidence / owner packet 準備度 | +| 平均只讀控管成熟度 | `65%` | 只代表框架 / evidence / owner packet 準備度 | | 需要 live evidence 類別 | `6` | 只能等 owner-provided redacted evidence 或維護窗口,不主動 SSH | | owner response required | `14` | owner response received / accepted 仍 `0 / 0` | | runtime gate | `0` | 不提供執行按鈕 | @@ -45,6 +45,12 @@ 此更新仍不是 live host truth:110 / 188 live hash、restart window、rollback owner、post-check 指標與 owner response received / accepted 全部仍為 `0`,也不得執行 `docker compose`、`systemctl`、repair-bot、Ansible apply 或任何 SSH 讀寫。 +### 0.3 2026-06-11 SSH / network access repo-only 清冊 + +`ssh_network_access_inventory_v1` 已把 SSH target、known_hosts workflow、CI deploy SSH、monitoring SSH、backup SSH capture、sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 alert SSH action catalog 納入只讀 snapshot。清冊目前共有 `16` 個 surface、`11` 個 SSH source surface、`6` 個 write-capable surface、`2` 個 NetworkPolicy、`2` 個 NodePort、`1` 個 sudoers surface 與 `1` 個 WireGuard surface,讓 SSH / network 類別成熟度從 `48%` 推進到 `54%`。 + +此更新仍不是 live network truth:live firewall、sudoers、known_hosts、NetworkPolicy、NodePort、WireGuard evidence、network owner、maintenance window、rollback owner 與 owner response received / accepted 全部仍為 `0`,也不得執行 SSH、keyscan、sudo、firewall change、NetworkPolicy apply、NodePort change 或 WireGuard cutover。 + ## 1. 目前已不符合新要求的項目 | 優先 | 項目 | 現況 | 風險 | 本階段處置 | diff --git a/docs/security/IWOOOS-POSTURE-PROJECTION.md b/docs/security/IWOOOS-POSTURE-PROJECTION.md index fa088529b..5a59e9aaa 100644 --- a/docs/security/IWOOOS-POSTURE-PROJECTION.md +++ b/docs/security/IWOOOS-POSTURE-PROJECTION.md @@ -89,8 +89,9 @@ IwoooS 首版只讀取或對齊以下已提交 evidence: 51. 10 個 frontend surface reverse bridge statuses,顯示既有資安入口目前是 embedded bridge、direct bridge 或 AwoooP read-only candidate;這只是連接狀態,不代表 owner response、runtime authorization、Code Review blocker、Gitea/GitHub action 或任何執行控制。 52. 6 個 source control primary readiness items,顯示 GitHub primary 前置缺口:candidate repo inventory、primary ready counter、owner response validation、refs truth、workflow / secret name inventory、rollback ADR;這只是 readiness,不代表 repo 建立、visibility 變更、refs mutation、secret value collection、primary switch 或 Gitea 停用。 53. 4 個 rollout risk read-only items,顯示風險來源部署 marker、`AWOOOI_ROLLOUT_RISK=1`、ArgoCD `Degraded` / `OutOfSync`、API health / smoke 已通過與執行期閘門仍為 0;這只是部署風險可見性,不代表 ArgoCD sync、kubectl、主機重啟、修復、部署或 runtime gate 已授權。 -54. 14 類 high-value config control coverage statuses,顯示 Nginx、DNS / TLS、K8s、secret、workflow、backup、agent-bounty runtime、monitoring、Docker / systemd、SSH / network、AI provider、產品 route 與 security evidence 的全域配置控管覆蓋矩陣;平均只讀成熟度 `64%`、C0 類別 `8`、需 live evidence 類別 `6`、owner response received / accepted 與 runtime gate 仍為 `0`,不代表 reload、sync、scan、secret rotation、payout 或主機操作授權。 +54. 14 類 high-value config control coverage statuses,顯示 Nginx、DNS / TLS、K8s、secret、workflow、backup、agent-bounty runtime、monitoring、Docker / systemd、SSH / network、AI provider、產品 route 與 security evidence 的全域配置控管覆蓋矩陣;平均只讀成熟度 `65%`、C0 類別 `8`、需 live evidence 類別 `6`、owner response received / accepted 與 runtime gate 仍為 `0`,不代表 reload、sync、scan、secret rotation、payout 或主機操作授權。 55. 9 個 host-service config repo-only inventory surfaces,顯示 Docker Compose、systemd / repair-bot、Ansible service role 與 host config backup capture 的第一層清冊;write-capable surface `3`、repair-bot whitelist `2`、systemd restart surface `1`,owner response、live evidence、restart window、rollback owner、runtime gate 與 action button 仍全部為 `0`,不代表 `docker compose`、`systemctl`、repair-bot 或 Ansible apply 已授權。 +56. 16 個 SSH / network access repo-only inventory surfaces,顯示 SSH target、known_hosts workflow、CI deploy SSH、monitoring SSH、backup SSH capture、sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 alert SSH action catalog 的第一層清冊;write-capable surface `6`、NetworkPolicy `2`、NodePort `2`、sudoers `1`、WireGuard `1`,owner response、live evidence、maintenance window、rollback owner、runtime gate 與 action button 仍全部為 `0`,不代表 SSH、sudo、firewall、NetworkPolicy、NodePort、WireGuard 或 known_hosts patch 已授權。 ## 3.1 既有前端資安頁面整合 diff --git a/docs/security/SSH-NETWORK-ACCESS-INVENTORY.md b/docs/security/SSH-NETWORK-ACCESS-INVENTORY.md new file mode 100644 index 000000000..6d46f9259 --- /dev/null +++ b/docs/security/SSH-NETWORK-ACCESS-INVENTORY.md @@ -0,0 +1,111 @@ +# IwoooS SSH / network access 只讀清冊 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-11 | +| 狀態 | `repo_only_inventory_ready` | +| 工具 | `scripts/security/ssh-network-access-inventory.py` | +| Snapshot | `docs/security/ssh-network-access-inventory.snapshot.json` | +| Schema | `docs/schemas/ssh_network_access_inventory_v1.schema.json` | +| runtime gate | `0` | + +## 1. 目的 + +這份清冊補齊高價值配置覆蓋矩陣中的 `ssh_firewall_network_access` 類別,把 repo 內會影響 SSH、sudoers、known_hosts、firewall / NetworkPolicy、NodePort 與 WireGuard 的配置來源先集中成可重跑 snapshot。 + +本階段仍是 repo-only 只讀清冊。它不是 live host truth,不是 firewall approval,不是 known_hosts patch approval,不是 NetworkPolicy apply approval,也不是 WireGuard cutover approval。 + +## 2. 覆蓋摘要 + +| 指標 | 目前值 | 說明 | +|------|--------|------| +| repo surface | `16` | 已納入 SSH / network access 相關 committed source | +| source exists / hash | `16` | 每個 source path 皆存在並有 SHA-256 | +| expected scope | `16` | 已整理每個 surface 的預期影響範圍 | +| SSH source surface | `11` | 包含 inventory、CI deploy、monitoring、backup、alert action | +| NetworkPolicy surface | `2` | production 與 ArgoCD metrics policy | +| NodePort surface | `2` | ArgoCD metrics 與 Velero metrics | +| sudoers surface | `1` | `awoooi-wrapper.sudoers` | +| WireGuard surface | `1` | GCP Ollama WireGuard mesh runbook | +| write-capable surface | `6` | CI deploy、monitoring deploy、sudoers、alert action catalog | +| owner response received / accepted | `0 / 0` | 尚未收到或接受 owner response | +| live evidence received | `0` | 尚未取得 owner-provided live evidence | +| runtime / action | `0 / 0` | 未開 runtime gate,未提供操作按鈕 | +| SSH / network 類別成熟度 | `48% -> 54%` | 只代表 repo-only 清冊完成,不代表 live 授權 | + +## 3. 已納入 surface + +| Surface | 類型 | 範圍 | 寫入能力 | +|---------|------|------|----------| +| `ansible_inventory_ssh_targets` | SSH target inventory | `110_111_112_120_121_188` | 否 | +| `ansible_common_ssh_args` | SSH client policy | `multi_host` | 否 | +| `gitea_cd_known_hosts_secret` | known_hosts workflow | `110_120_121_188_known_hosts` | 否 | +| `gitea_cd_deploy_ssh` | CI deploy SSH | `k8s_ssh_host` | 是 | +| `gitea_cd_dev_ssh` | CI deploy SSH | `192.168.0.120` | 是 | +| `deploy_alerts_ssh_path` | CI deploy SSH | `192.168.0.110` | 是 | +| `monitoring_discover_docker_ssh` | SSH discovery script | `110_188_docker_hosts` | 否 | +| `monitoring_exporter_deploy_ssh` | monitoring SSH deploy script | `192.168.0.188` | 是 | +| `backup_config_ssh_capture` | SSH backup capture | `110_188_120_121_cluster` | 否 | +| `host_ops_sudoers_wrapper` | sudoers policy | `host_ops_minimal_sudo` | 是 | +| `k8s_prod_network_policy` | K8s NetworkPolicy | `awoooi_prod_namespace` | 否 | +| `argocd_metrics_network_policy` | K8s NetworkPolicy | `argocd_namespace` | 否 | +| `argocd_metrics_nodeport` | K8s NodePort service | `argocd_nodeport_30882_30883` | 否 | +| `velero_metrics_nodeport` | K8s NodePort service | `velero_nodeport_30885` | 否 | +| `wireguard_mesh_runbook` | WireGuard runbook | `110_111_120_121_gcp_a_gcp_b` | 否 | +| `alert_rules_ssh_actions` | alert SSH action rules | `ssh_mcp_action_catalog` | 是 | + +## 4. 固定 0 / false 邊界 + +```text +runtime_execution_authorized=false +host_write_authorized=false +ssh_read_authorized=false +ssh_write_authorized=false +sudo_action_authorized=false +firewall_change_authorized=false +network_policy_apply_authorized=false +nodeport_change_authorized=false +wireguard_change_authorized=false +known_hosts_patch_authorized=false +host_keyscan_authorized=false +live_host_read_authorized=false +secret_value_collection_allowed=false +ssh_key_collection_allowed=false +active_scan_authorized=false +action_buttons_allowed=false +``` + +## 5. 判讀規則 + +1. `source_exists=true` 只代表 repo 檔案存在,不代表 live host 與 repo 一致。 +2. `sha256` 是 committed source 的 hash,不是 live `/etc/ssh`、firewall、sudoers、NetworkPolicy 或 WireGuard hash。 +3. `write_capable_surface_count=6` 代表需要 owner review 的高風險入口,不代表可執行。 +4. `accept-new`、known_hosts、NodePort、NetworkPolicy 與 WireGuard 只能先形成 owner 問題,不得自動 patch、keyscan、apply 或 cutover。 +5. 後續若要取得 live evidence,只能走 owner-provided redacted evidence、維護窗口與 rollback owner;不得在本階段主動 SSH、sudo、掃描或讀 secret。 + +## 6. 指令 + +```bash +python3 scripts/security/ssh-network-access-inventory.py \ + --root . \ + --output docs/security/ssh-network-access-inventory.snapshot.json +``` + +固定 committed snapshot 時間: + +```bash +python3 scripts/security/ssh-network-access-inventory.py \ + --root . \ + --generated-at 2026-06-11T23:55:00+08:00 \ + --output docs/security/ssh-network-access-inventory.snapshot.json +``` + +## 7. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| repo-only surface 註冊 | `100%` | 已納入 16 個 SSH / network access surface | +| source existence / hash | `100%` | 16 個 source path 皆已驗證存在並產生 hash | +| owner response 收件 | `0%` | 尚未收到或接受 owner response | +| live evidence collection | `0%` | 未 SSH、未 keyscan、未讀 live firewall、未讀 live sudoers | +| SSH / sudo / firewall / NetworkPolicy / NodePort / WireGuard gate | `0%` | 全部維持未授權 | diff --git a/docs/security/high-value-config-control-coverage.snapshot.json b/docs/security/high-value-config-control-coverage.snapshot.json index 1a8e13fa7..664881b05 100644 --- a/docs/security/high-value-config-control-coverage.snapshot.json +++ b/docs/security/high-value-config-control-coverage.snapshot.json @@ -365,15 +365,17 @@ "action_buttons_allowed": false, "category_id": "ssh_firewall_network_access", "control_tier": "C1", - "coverage_percent": 48, - "coverage_status": "policy_ready_needs_network_matrix", - "current_gap": "尚未集中 ingress / egress、WireGuard、NodePort、firewall 與 known_hosts 矩陣。", + "coverage_percent": 54, + "coverage_status": "repo_only_inventory_ready_needs_live_owner_evidence", + "current_gap": "repo-only 清冊已納入 16 個 SSH / network access surface;仍缺 live firewall / sudoers / known_hosts / NetworkPolicy / NodePort / WireGuard evidence、network owner 與 rollback owner。", "evidence_refs": [ "docs/HARD_RULES.md", - "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md" + "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md", + "docs/security/SSH-NETWORK-ACCESS-INVENTORY.md", + "docs/security/ssh-network-access-inventory.snapshot.json" ], "label": "SSH / sudoers / known_hosts / firewall / WireGuard / NodePort", - "next_owner_action": "補 target whitelist、host key policy、network owner、maintenance window 與 rollback owner。", + "next_owner_action": "補 owner-provided live hash / disposition、host key pinning、firewall owner、NetworkPolicy / NodePort owner、WireGuard owner、maintenance window、rollback owner 與 post-check 指標。", "owner_response_accepted": false, "owner_response_received": false, "owner_response_required": true, @@ -520,16 +522,9 @@ "secret_value_collection_allowed": false, "workflow_modification_authorized": false }, - "generated_at": "2026-06-11T23:21:00+08:00", - "git_commit": "0a82648e", + "generated_at": "2026-06-12T00:05:00+08:00", + "git_commit": "7cd47558", "lowest_coverage_categories": [ - { - "category_id": "ssh_firewall_network_access", - "coverage_percent": 48, - "current_gap": "尚未集中 ingress / egress、WireGuard、NodePort、firewall 與 known_hosts 矩陣。", - "label": "SSH / sudoers / known_hosts / firewall / WireGuard / NodePort", - "next_owner_action": "補 target whitelist、host key policy、network owner、maintenance window 與 rollback owner。" - }, { "category_id": "docker_compose_systemd_host_config", "coverage_percent": 50, @@ -544,6 +539,13 @@ "label": "Backup / restore / escrow / retention", "next_owner_action": "補 restore drill approval package、escrow owner、retention owner 與 no-secret-value evidence。" }, + { + "category_id": "ssh_firewall_network_access", + "coverage_percent": 54, + "current_gap": "repo-only 清冊已納入 16 個 SSH / network access surface;仍缺 live firewall / sudoers / known_hosts / NetworkPolicy / NodePort / WireGuard evidence、network owner 與 rollback owner。", + "label": "SSH / sudoers / known_hosts / firewall / WireGuard / NodePort", + "next_owner_action": "補 owner-provided live hash / disposition、host key pinning、firewall owner、NetworkPolicy / NodePort owner、WireGuard owner、maintenance window、rollback owner 與 post-check 指標。" + }, { "category_id": "monitoring_alerting_observability", "coverage_percent": 56, @@ -573,7 +575,7 @@ "status": "coverage_matrix_ready", "summary": { "action_button_count": 0, - "average_coverage_percent": 64, + "average_coverage_percent": 65, "c0_category_count": 8, "c1_category_count": 4, "c2_category_count": 1, diff --git a/docs/security/iwooos-posture-projection.snapshot.json b/docs/security/iwooos-posture-projection.snapshot.json index 572e5ff37..bdbdcbbdc 100644 --- a/docs/security/iwooos-posture-projection.snapshot.json +++ b/docs/security/iwooos-posture-projection.snapshot.json @@ -28,6 +28,9 @@ "docs/security/host-service-config-inventory.snapshot.json", "docs/security/HOST-SERVICE-CONFIG-INVENTORY.md", "docs/schemas/host_service_config_inventory_v1.schema.json", + "docs/security/ssh-network-access-inventory.snapshot.json", + "docs/security/SSH-NETWORK-ACCESS-INVENTORY.md", + "docs/schemas/ssh_network_access_inventory_v1.schema.json", "docs/LOGBOOK.md", "docs/workplans/2026-06-04-iwooos-security-governance-p0.md", "apps/web/src/app/[locale]/iwooos/page.tsx", @@ -236,7 +239,7 @@ "high_value_config_control_coverage_category_count": 14, "high_value_config_control_coverage_c0_category_count": 8, "high_value_config_control_coverage_c1_category_count": 4, - "high_value_config_control_coverage_average_percent": 64, + "high_value_config_control_coverage_average_percent": 65, "high_value_config_control_coverage_needs_live_evidence_count": 6, "high_value_config_control_coverage_owner_response_required_count": 14, "high_value_config_control_coverage_owner_response_received_count": 0, @@ -263,6 +266,27 @@ "host_service_config_inventory_action_button_count": 0, "host_service_config_inventory_coverage_percent_before_inventory": 42, "host_service_config_inventory_coverage_percent_after_inventory": 50, + "ssh_network_access_inventory_first_layer": true, + "ssh_network_access_inventory_surface_count": 16, + "ssh_network_access_inventory_source_exists_count": 16, + "ssh_network_access_inventory_expected_scope_count": 16, + "ssh_network_access_inventory_ssh_source_surface_count": 11, + "ssh_network_access_inventory_network_policy_surface_count": 2, + "ssh_network_access_inventory_nodeport_surface_count": 2, + "ssh_network_access_inventory_sudoers_surface_count": 1, + "ssh_network_access_inventory_wireguard_surface_count": 1, + "ssh_network_access_inventory_write_capable_surface_count": 6, + "ssh_network_access_inventory_owner_response_required_count": 16, + "ssh_network_access_inventory_owner_response_received_count": 0, + "ssh_network_access_inventory_owner_response_accepted_count": 0, + "ssh_network_access_inventory_live_evidence_required_count": 16, + "ssh_network_access_inventory_live_evidence_received_count": 0, + "ssh_network_access_inventory_maintenance_window_accepted_count": 0, + "ssh_network_access_inventory_rollback_owner_accepted_count": 0, + "ssh_network_access_inventory_runtime_gate_count": 0, + "ssh_network_access_inventory_action_button_count": 0, + "ssh_network_access_inventory_coverage_percent_before_inventory": 48, + "ssh_network_access_inventory_coverage_percent_after_inventory": 54, "high_value_config_owner_packet_first_layer": true, "high_value_config_owner_packet_summary_count": 4, "high_value_config_owner_packet_item_count": 6, diff --git a/docs/security/ssh-network-access-inventory.snapshot.json b/docs/security/ssh-network-access-inventory.snapshot.json new file mode 100644 index 000000000..815157686 --- /dev/null +++ b/docs/security/ssh-network-access-inventory.snapshot.json @@ -0,0 +1,571 @@ +{ + "access_surfaces": [ + { + "access_scope": [ + "192.168.0.110", + "192.168.0.111", + "192.168.0.112", + "192.168.0.120", + "192.168.0.121", + "192.168.0.188" + ], + "action_buttons_allowed": false, + "config_kind": "ssh_target_inventory", + "control_tier": "C1", + "current_state": "repo_source_visible_needs_pinned_host_key_policy", + "expected_scope": "110_111_112_120_121_188", + "label": "Ansible inventory SSH targets", + "line_count": 48, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 host owner、pinned known_hosts disposition、ProxyJump policy、key owner 與 rollback owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "86108dce9174b5c0a794d240dd40518966d9c340950fc6306845b704f12e6536", + "source_exists": true, + "source_path": "infra/ansible/inventory/hosts.yml", + "surface_id": "ansible_inventory_ssh_targets" + }, + { + "access_scope": [ + "StrictHostKeyChecking=accept-new", + "ConnectTimeout=10" + ], + "action_buttons_allowed": false, + "config_kind": "ssh_client_policy", + "control_tier": "C1", + "current_state": "accept_new_policy_visible_needs_owner_disposition", + "expected_scope": "multi_host", + "label": "Ansible common SSH host key policy", + "line_count": 20, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "確認 accept-new 是否只限 bootstrap,補升級 pinned known_hosts 的 owner 與時間窗。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "c3d5cb63cf84dea98195aa075e69ca90be7422b5805c0cfc50c1d97b832ad86e", + "source_exists": true, + "source_path": "infra/ansible/inventory/group_vars/all.yml", + "surface_id": "ansible_common_ssh_args" + }, + { + "access_scope": [ + "192.168.0.110", + "192.168.0.120", + "192.168.0.121", + "192.168.0.188" + ], + "action_buttons_allowed": false, + "config_kind": "known_hosts_secret_workflow", + "control_tier": "C1", + "current_state": "repo_guard_visible_live_secret_not_verified", + "expected_scope": "110_120_121_188_known_hosts", + "label": "Gitea CD repair known_hosts secret", + "line_count": 1562, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 owner-provided known_hosts secret metadata、缺 120 時的處置、key rotation owner 與失敗通知 owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "5b41cdc34c954a383ebea9e4109d10165ceb40589d55df9ee6e808d4092bf593", + "source_exists": true, + "source_path": ".gitea/workflows/cd.yaml", + "surface_id": "gitea_cd_known_hosts_secret" + }, + { + "access_scope": [ + "K8S_SSH_HOST", + "deploy_key", + "kubectl apply", + "ArgoCD sync" + ], + "action_buttons_allowed": false, + "config_kind": "ci_deploy_ssh", + "control_tier": "C1", + "current_state": "write_capable_deploy_path_visible_gate_closed", + "expected_scope": "k8s_ssh_host", + "label": "Gitea CD K8s deploy SSH path", + "line_count": 1562, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 deploy SSH host owner、maintenance window、rollback owner、post-check 指標與 break-glass policy。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "5b41cdc34c954a383ebea9e4109d10165ceb40589d55df9ee6e808d4092bf593", + "source_exists": true, + "source_path": ".gitea/workflows/cd.yaml", + "surface_id": "gitea_cd_deploy_ssh" + }, + { + "access_scope": [ + "192.168.0.120", + "deploy_key", + "kubectl apply" + ], + "action_buttons_allowed": false, + "config_kind": "ci_deploy_ssh", + "control_tier": "C1", + "current_state": "dev_write_capable_deploy_path_visible_gate_closed", + "expected_scope": "192.168.0.120", + "label": "Gitea CD dev deploy SSH path", + "line_count": 262, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "確認 dev deploy key scope、host key policy、rollback owner 與 dev/prod 邊界。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "e344a4672cb543979c3bb8ea67967c103332587b4a52a939c837457aaeae686d", + "source_exists": true, + "source_path": ".gitea/workflows/cd-dev.yaml", + "surface_id": "gitea_cd_dev_ssh" + }, + { + "access_scope": [ + "192.168.0.110", + "deploy alert scripts" + ], + "action_buttons_allowed": false, + "config_kind": "ci_deploy_ssh", + "control_tier": "C1", + "current_state": "write_capable_alert_deploy_path_visible_gate_closed", + "expected_scope": "192.168.0.110", + "label": "Deploy alerts SSH path", + "line_count": 72, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 alert deploy owner、known_hosts pinning、rollback owner、post-check 與通知路徑。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "b0389fa65da643d411f6961928a276d555ad6a416366bf87f3f5c2c06ee45d13", + "source_exists": true, + "source_path": ".gitea/workflows/deploy-alerts.yaml", + "surface_id": "deploy_alerts_ssh_path" + }, + { + "access_scope": [ + "192.168.0.110", + "192.168.0.188", + "docker ps" + ], + "action_buttons_allowed": false, + "config_kind": "ssh_discovery_script", + "control_tier": "C1", + "current_state": "accept_new_scanner_visible_needs_read_only_gate", + "expected_scope": "110_188_docker_hosts", + "label": "Monitoring Docker discovery SSH scanner", + "line_count": 314, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 scanner 執行 owner、read-only window、pinned known_hosts、輸出脫敏與失敗處置。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "563faf8efcfdbd5a79cc87e0d43c2ba11bebf755a773c97b9c0778f1f0634a15", + "source_exists": true, + "source_path": "ops/monitoring/discover_docker.py", + "surface_id": "monitoring_discover_docker_ssh" + }, + { + "access_scope": [ + "192.168.0.188", + "scp", + "docker compose up -d" + ], + "action_buttons_allowed": false, + "config_kind": "monitoring_ssh_deploy_script", + "control_tier": "C1", + "current_state": "write_capable_script_visible_gate_closed", + "expected_scope": "192.168.0.188", + "label": "Monitoring exporter deploy SSH script", + "line_count": 76, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 exporter deploy owner、maintenance window、rollback owner、host key policy 與 post-check 指標。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "dbcbca21cf6fd5083177cb8a12c008c1aefed8e6ed05b70d738b3db37699cef3", + "source_exists": true, + "source_path": "ops/monitoring/deploy-exporters.sh", + "surface_id": "monitoring_exporter_deploy_ssh" + }, + { + "access_scope": [ + "/etc/ssh", + "/etc/nginx", + "systemd", + "docker", + "k8s" + ], + "action_buttons_allowed": false, + "config_kind": "ssh_backup_capture", + "control_tier": "C1", + "current_state": "read_capable_capture_visible_not_executed", + "expected_scope": "110_188_120_121_cluster", + "label": "Backup config SSH capture", + "line_count": 359, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 backup execution owner、secret redaction proof、retention owner 與 restore validation。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "d24301cff44e464bd19ce0792362be16916ccde8c92f92351a19ef4ee988f15e", + "source_exists": true, + "source_path": "scripts/backup/backup-configs.sh", + "surface_id": "backup_config_ssh_capture" + }, + { + "access_scope": [ + "awoooi-hosts-add", + "docker kill SIGHUP", + "promtool", + "amtool" + ], + "action_buttons_allowed": false, + "config_kind": "sudoers_policy", + "control_tier": "C1", + "current_state": "sudoers_source_visible_needs_live_owner_evidence", + "expected_scope": "host_ops_minimal_sudo", + "label": "Host ops sudoers wrapper", + "line_count": 27, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 live sudoers hash、visudo validation、command owner、rollback owner 與 forbidden command proof。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "eff02c67402d2f5b2ac8d112dca26a15dc34f03593ca490a0682a6dfa9b0394d", + "source_exists": true, + "source_path": "scripts/host-ops/awoooi-wrapper.sudoers", + "surface_id": "host_ops_sudoers_wrapper" + }, + { + "access_scope": [ + "default deny", + "ingress", + "egress", + "SSH egress", + "Ollama", + "monitoring" + ], + "action_buttons_allowed": false, + "config_kind": "k8s_network_policy", + "control_tier": "C1", + "current_state": "repo_policy_visible_needs_live_cluster_diff", + "expected_scope": "awoooi_prod_namespace", + "label": "K8s production NetworkPolicy", + "line_count": 306, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 live NetworkPolicy diff、ingress / egress owner、rollback owner 與 route smoke。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "f5ea6a9f5fb0cc44664d97a3ed639fa4b43ffd9bcfd70a1f6b44640791b7859f", + "source_exists": true, + "source_path": "k8s/awoooi-prod/02-network-policy.yaml", + "surface_id": "k8s_prod_network_policy" + }, + { + "access_scope": [ + "192.168.0.188", + "argocd metrics", + "192.168.0.0/24 UI" + ], + "action_buttons_allowed": false, + "config_kind": "k8s_network_policy", + "control_tier": "C1", + "current_state": "repo_policy_visible_needs_prometheus_owner_confirmation", + "expected_scope": "argocd_namespace", + "label": "ArgoCD metrics NetworkPolicy", + "line_count": 80, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 Prometheus scrape owner、NodePort exposure owner、live policy diff 與 rollback owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "41ccd0bb22410c48adc84eae74391106c3f28fe181786cfe4128a07f99d2942c", + "source_exists": true, + "source_path": "k8s/argocd/argocd-metrics-network-policy.yaml", + "surface_id": "argocd_metrics_network_policy" + }, + { + "access_scope": [ + "nodePort 30882", + "nodePort 30883" + ], + "action_buttons_allowed": false, + "config_kind": "k8s_nodeport_service", + "control_tier": "C1", + "current_state": "nodeport_source_visible_needs_exposure_review", + "expected_scope": "argocd_nodeport_30882_30883", + "label": "ArgoCD metrics NodePort", + "line_count": 47, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 NodePort exposure owner、firewall owner、Prometheus source whitelist 與 rollback owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "7f4a8f09206ce0afc185fe11d5e55265bb553b671471724cdcd83c259ec7d266", + "source_exists": true, + "source_path": "k8s/argocd/argocd-metrics-nodeport.yaml", + "surface_id": "argocd_metrics_nodeport" + }, + { + "access_scope": [ + "nodePort 30885", + "backup metrics" + ], + "action_buttons_allowed": false, + "config_kind": "k8s_nodeport_service", + "control_tier": "C1", + "current_state": "nodeport_source_visible_needs_exposure_review", + "expected_scope": "velero_nodeport_30885", + "label": "Velero metrics NodePort", + "line_count": 26, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 Velero metrics exposure owner、firewall owner、Prometheus source whitelist 與 rollback owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "684959def32b792e2bca34b477afcdfe2b0c6dfd0cb90f4b681a514922d62b75", + "source_exists": true, + "source_path": "k8s/velero/velero-metrics-service.yaml", + "surface_id": "velero_metrics_nodeport" + }, + { + "access_scope": [ + "10.77.114.0/24", + "51820/udp", + "GCP-A", + "GCP-B" + ], + "action_buttons_allowed": false, + "config_kind": "wireguard_runbook", + "control_tier": "C1", + "current_state": "target_architecture_documented_not_applied", + "expected_scope": "110_111_120_121_gcp_a_gcp_b", + "label": "GCP Ollama WireGuard mesh runbook", + "line_count": 280, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 WireGuard owner、public-key metadata、firewall rule owner、canary plan、rollback owner 與 cutover gate。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "0af082698c727176ca82c79f95f3950f4c32ed6aabc91c88aff41831fbf0c044", + "source_exists": true, + "source_path": "docs/runbooks/GCP-OLLAMA-WIREGUARD-MESH.md", + "surface_id": "wireguard_mesh_runbook" + }, + { + "access_scope": [ + "ssh_diagnose", + "docker restart", + "systemctl restart", + "docker compose", + "docker prune" + ], + "action_buttons_allowed": false, + "config_kind": "alert_ssh_action_rules", + "control_tier": "C1", + "current_state": "ssh_action_catalog_visible_gate_closed", + "expected_scope": "ssh_mcp_action_catalog", + "label": "Alert rules SSH action surface", + "line_count": 889, + "live_evidence_received": false, + "maintenance_window_accepted": false, + "next_owner_action": "補 action owner、read/write/admin 分級、approval gate、cooldown、rollback owner 與 post-check 指標。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "sha256": "5786505aa05073bbb2069203a443a75c8337a289dc015630792d0c201c85cafb", + "source_exists": true, + "source_path": "apps/api/alert_rules.yaml", + "surface_id": "alert_rules_ssh_actions" + } + ], + "execution_boundaries": { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "firewall_change_authorized": false, + "host_keyscan_authorized": false, + "host_write_authorized": false, + "known_hosts_patch_authorized": false, + "live_host_read_authorized": false, + "network_policy_apply_authorized": false, + "nodeport_change_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "ssh_key_collection_allowed": false, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "sudo_action_authorized": false, + "wireguard_change_authorized": false + }, + "expected_scopes": [ + "110_111_112_120_121_188", + "110_111_120_121_gcp_a_gcp_b", + "110_120_121_188_known_hosts", + "110_188_120_121_cluster", + "110_188_docker_hosts", + "192.168.0.110", + "192.168.0.120", + "192.168.0.188", + "argocd_namespace", + "argocd_nodeport_30882_30883", + "awoooi_prod_namespace", + "host_ops_minimal_sudo", + "k8s_ssh_host", + "multi_host", + "ssh_mcp_action_catalog", + "velero_nodeport_30885" + ], + "generated_at": "2026-06-11T23:55:00+08:00", + "git_commit": "7cd47558", + "next_collection_order": [ + "gitea_cd_known_hosts_secret", + "ansible_inventory_ssh_targets", + "host_ops_sudoers_wrapper", + "k8s_prod_network_policy", + "alert_rules_ssh_actions", + "argocd_metrics_nodeport", + "velero_metrics_nodeport", + "wireguard_mesh_runbook", + "monitoring_discover_docker_ssh", + "backup_config_ssh_capture" + ], + "operator_interpretation": [ + "這是 repo-only SSH / network access 清冊,不是 live host、firewall 或 cluster truth。", + "source_exists=true 只代表 repo 檔案存在;不代表 known_hosts、sudoers、NetworkPolicy、NodePort 或 WireGuard 已套用。", + "write-capable SSH / sudoers / alert action surface 可見代表需被管控,不代表 SSH、sudo、docker、systemctl 或 kubectl 已授權。", + "所有 live hash、pinned host key、maintenance window、rollback owner、firewall owner 與 post-check 指標都仍需 owner response。" + ], + "schema_version": "ssh_network_access_inventory_v1", + "source_scope": "committed_repo_files_only", + "status": "repo_only_inventory_ready", + "summary": { + "action_button_count": 0, + "coverage_percent_after_inventory": 54, + "coverage_percent_before_inventory": 48, + "expected_scope_count": 16, + "live_evidence_received_count": 0, + "maintenance_window_accepted_count": 0, + "network_policy_surface_count": 2, + "nodeport_surface_count": 2, + "owner_response_accepted_count": 0, + "owner_response_received_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "source_exists_count": 16, + "ssh_source_surface_count": 11, + "sudoers_surface_count": 1, + "surface_count": 16, + "surfaces_requiring_live_evidence_count": 16, + "surfaces_requiring_owner_response_count": 16, + "wireguard_surface_count": 1, + "write_capable_surface_count": 6 + }, + "write_capable_surfaces": [ + { + "config_kind": "ci_deploy_ssh", + "expected_scope": "k8s_ssh_host", + "label": "Gitea CD K8s deploy SSH path", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "surface_id": "gitea_cd_deploy_ssh" + }, + { + "config_kind": "ci_deploy_ssh", + "expected_scope": "192.168.0.120", + "label": "Gitea CD dev deploy SSH path", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "surface_id": "gitea_cd_dev_ssh" + }, + { + "config_kind": "ci_deploy_ssh", + "expected_scope": "192.168.0.110", + "label": "Deploy alerts SSH path", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "surface_id": "deploy_alerts_ssh_path" + }, + { + "config_kind": "monitoring_ssh_deploy_script", + "expected_scope": "192.168.0.188", + "label": "Monitoring exporter deploy SSH script", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "surface_id": "monitoring_exporter_deploy_ssh" + }, + { + "config_kind": "sudoers_policy", + "expected_scope": "host_ops_minimal_sudo", + "label": "Host ops sudoers wrapper", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "surface_id": "host_ops_sudoers_wrapper" + }, + { + "config_kind": "alert_ssh_action_rules", + "expected_scope": "ssh_mcp_action_catalog", + "label": "Alert rules SSH action surface", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "surface_id": "alert_rules_ssh_actions" + } + ] +} diff --git a/scripts/security/high-value-config-control-coverage.py b/scripts/security/high-value-config-control-coverage.py index be9b97cb4..d008d127b 100644 --- a/scripts/security/high-value-config-control-coverage.py +++ b/scripts/security/high-value-config-control-coverage.py @@ -131,14 +131,16 @@ CONTROL_STATUS_BY_CATEGORY = { "next_owner_action": "補 owner-provided live hash / disposition、compose / systemd owner、restart window、rollback owner 與 post-check 指標。", }, "ssh_firewall_network_access": { - "coverage_status": "policy_ready_needs_network_matrix", - "coverage_percent": 48, + "coverage_status": "repo_only_inventory_ready_needs_live_owner_evidence", + "coverage_percent": 54, "evidence_refs": [ "docs/HARD_RULES.md", "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md", + "docs/security/SSH-NETWORK-ACCESS-INVENTORY.md", + "docs/security/ssh-network-access-inventory.snapshot.json", ], - "current_gap": "尚未集中 ingress / egress、WireGuard、NodePort、firewall 與 known_hosts 矩陣。", - "next_owner_action": "補 target whitelist、host key policy、network owner、maintenance window 與 rollback owner。", + "current_gap": "repo-only 清冊已納入 16 個 SSH / network access surface;仍缺 live firewall / sudoers / known_hosts / NetworkPolicy / NodePort / WireGuard evidence、network owner 與 rollback owner。", + "next_owner_action": "補 owner-provided live hash / disposition、host key pinning、firewall owner、NetworkPolicy / NodePort owner、WireGuard owner、maintenance window、rollback owner 與 post-check 指標。", }, "ai_provider_model_routing": { "coverage_status": "policy_ready_needs_dry_run_pack", diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 18e6dbd91..a3c2f3f69 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -89,6 +89,7 @@ def validate(root: Path) -> None: iwooos_projection = load_json(security_dir / "iwooos-posture-projection.snapshot.json") high_value_config_coverage = load_json(security_dir / "high-value-config-control-coverage.snapshot.json") host_service_config_inventory = load_json(security_dir / "host-service-config-inventory.snapshot.json") + ssh_network_access_inventory = load_json(security_dir / "ssh-network-access-inventory.snapshot.json") domain_tls_inventory = load_json(security_dir / "domain-tls-certbot-inventory.snapshot.json") s49_request_draft = load_json(security_dir / "gitea-inventory-owner-attestation-request-draft.snapshot.json") kali_status = load_json(security_dir / "kali-integration-status.snapshot.json") @@ -2418,7 +2419,7 @@ def validate(root: Path) -> None: assert_equal( "high_value_config_coverage.summary.average_coverage_percent", high_value_config_coverage["summary"]["average_coverage_percent"], - 64, + 65, ) assert_equal( "high_value_config_coverage.summary.needs_live_evidence_count", @@ -2477,6 +2478,30 @@ def validate(root: Path) -> None: docker_systemd_category["evidence_refs"], evidence_ref, ) + ssh_network_category = next( + item + for item in high_value_config_coverage["coverage_categories"] + if item["category_id"] == "ssh_firewall_network_access" + ) + assert_equal( + "high_value_config_coverage.coverage_categories.ssh_network.coverage_percent", + ssh_network_category["coverage_percent"], + 54, + ) + assert_equal( + "high_value_config_coverage.coverage_categories.ssh_network.coverage_status", + ssh_network_category["coverage_status"], + "repo_only_inventory_ready_needs_live_owner_evidence", + ) + for evidence_ref in [ + "docs/security/SSH-NETWORK-ACCESS-INVENTORY.md", + "docs/security/ssh-network-access-inventory.snapshot.json", + ]: + assert_contains( + "high_value_config_coverage.coverage_categories.ssh_network.evidence_refs", + ssh_network_category["evidence_refs"], + evidence_ref, + ) assert_equal( "host_service_config_inventory.schema", host_service_config_inventory["schema_version"], @@ -2579,13 +2604,131 @@ def validate(root: Path) -> None: [item["surface_id"] for item in host_service_config_inventory["write_capable_surfaces"]], surface_id, ) + assert_equal( + "ssh_network_access_inventory.schema", + ssh_network_access_inventory["schema_version"], + "ssh_network_access_inventory_v1", + ) + assert_equal( + "ssh_network_access_inventory.status", + ssh_network_access_inventory["status"], + "repo_only_inventory_ready", + ) + assert_equal( + "ssh_network_access_inventory.source_scope", + ssh_network_access_inventory["source_scope"], + "committed_repo_files_only", + ) + expected_ssh_network_access_summary = { + "surface_count": 16, + "source_exists_count": 16, + "expected_scope_count": 16, + "ssh_source_surface_count": 11, + "network_policy_surface_count": 2, + "nodeport_surface_count": 2, + "sudoers_surface_count": 1, + "wireguard_surface_count": 1, + "write_capable_surface_count": 6, + "surfaces_requiring_owner_response_count": 16, + "surfaces_requiring_live_evidence_count": 16, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "live_evidence_received_count": 0, + "maintenance_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + "coverage_percent_before_inventory": 48, + "coverage_percent_after_inventory": 54, + } + for key, expected in expected_ssh_network_access_summary.items(): + assert_equal( + f"ssh_network_access_inventory.summary.{key}", + ssh_network_access_inventory["summary"][key], + expected, + ) + for key, value in ssh_network_access_inventory["execution_boundaries"].items(): + assert_false(f"ssh_network_access_inventory.execution_boundaries.{key}", value) + assert_equal( + "ssh_network_access_inventory.access_surfaces.count", + len(ssh_network_access_inventory["access_surfaces"]), + 16, + ) + ssh_network_surface_ids = [item["surface_id"] for item in ssh_network_access_inventory["access_surfaces"]] + for surface_id in [ + "ansible_inventory_ssh_targets", + "gitea_cd_known_hosts_secret", + "host_ops_sudoers_wrapper", + "k8s_prod_network_policy", + "argocd_metrics_nodeport", + "velero_metrics_nodeport", + "wireguard_mesh_runbook", + "alert_rules_ssh_actions", + ]: + assert_contains( + "ssh_network_access_inventory.access_surfaces", + ssh_network_surface_ids, + surface_id, + ) + for surface in ssh_network_access_inventory["access_surfaces"]: + assert_true( + f"ssh_network_access_inventory.access_surfaces.{surface['surface_id']}.source_exists", + surface["source_exists"], + ) + assert_equal( + f"ssh_network_access_inventory.access_surfaces.{surface['surface_id']}.sha256_length", + len(surface["sha256"]), + 64, + ) + assert_true( + f"ssh_network_access_inventory.access_surfaces.{surface['surface_id']}.requires_live_evidence", + surface["requires_live_evidence"], + ) + assert_true( + f"ssh_network_access_inventory.access_surfaces.{surface['surface_id']}.requires_owner_response", + surface["requires_owner_response"], + ) + for key in [ + "owner_response_received", + "owner_response_accepted", + "live_evidence_received", + "maintenance_window_accepted", + "rollback_owner_accepted", + "runtime_gate_open", + "action_buttons_allowed", + ]: + assert_false( + f"ssh_network_access_inventory.access_surfaces.{surface['surface_id']}.{key}", + surface[key], + ) + assert_equal( + "ssh_network_access_inventory.write_capable_surfaces.count", + len(ssh_network_access_inventory["write_capable_surfaces"]), + 6, + ) + for surface_id in [ + "gitea_cd_deploy_ssh", + "gitea_cd_dev_ssh", + "deploy_alerts_ssh_path", + "monitoring_exporter_deploy_ssh", + "host_ops_sudoers_wrapper", + "alert_rules_ssh_actions", + ]: + assert_contains( + "ssh_network_access_inventory.write_capable_surfaces", + [item["surface_id"] for item in ssh_network_access_inventory["write_capable_surfaces"]], + surface_id, + ) for source_path in [ "docs/security/host-service-config-inventory.snapshot.json", "docs/security/HOST-SERVICE-CONFIG-INVENTORY.md", "docs/schemas/host_service_config_inventory_v1.schema.json", + "docs/security/ssh-network-access-inventory.snapshot.json", + "docs/security/SSH-NETWORK-ACCESS-INVENTORY.md", + "docs/schemas/ssh_network_access_inventory_v1.schema.json", ]: assert_contains( - "iwooos_projection.source_paths.host_service_config_inventory", + "iwooos_projection.source_paths.config_inventory", iwooos_projection["source_paths"], source_path, ) @@ -2607,7 +2750,7 @@ def validate(root: Path) -> None: "high_value_config_control_coverage_category_count": 14, "high_value_config_control_coverage_c0_category_count": 8, "high_value_config_control_coverage_c1_category_count": 4, - "high_value_config_control_coverage_average_percent": 64, + "high_value_config_control_coverage_average_percent": 65, "high_value_config_control_coverage_needs_live_evidence_count": 6, "high_value_config_control_coverage_owner_response_required_count": 14, "high_value_config_control_coverage_owner_response_received_count": 0, @@ -2649,6 +2792,35 @@ def validate(root: Path) -> None: iwooos_projection["summary"][key], expected, ) + expected_ssh_network_projection_summary = { + "ssh_network_access_inventory_first_layer": True, + "ssh_network_access_inventory_surface_count": 16, + "ssh_network_access_inventory_source_exists_count": 16, + "ssh_network_access_inventory_expected_scope_count": 16, + "ssh_network_access_inventory_ssh_source_surface_count": 11, + "ssh_network_access_inventory_network_policy_surface_count": 2, + "ssh_network_access_inventory_nodeport_surface_count": 2, + "ssh_network_access_inventory_sudoers_surface_count": 1, + "ssh_network_access_inventory_wireguard_surface_count": 1, + "ssh_network_access_inventory_write_capable_surface_count": 6, + "ssh_network_access_inventory_owner_response_required_count": 16, + "ssh_network_access_inventory_owner_response_received_count": 0, + "ssh_network_access_inventory_owner_response_accepted_count": 0, + "ssh_network_access_inventory_live_evidence_required_count": 16, + "ssh_network_access_inventory_live_evidence_received_count": 0, + "ssh_network_access_inventory_maintenance_window_accepted_count": 0, + "ssh_network_access_inventory_rollback_owner_accepted_count": 0, + "ssh_network_access_inventory_runtime_gate_count": 0, + "ssh_network_access_inventory_action_button_count": 0, + "ssh_network_access_inventory_coverage_percent_before_inventory": 48, + "ssh_network_access_inventory_coverage_percent_after_inventory": 54, + } + for key, expected in expected_ssh_network_projection_summary.items(): + assert_equal( + f"iwooos_projection.summary.{key}", + iwooos_projection["summary"][key], + expected, + ) assert_true( "iwooos_projection.summary.high_value_config_owner_packet_first_layer", iwooos_projection["summary"]["high_value_config_owner_packet_first_layer"], @@ -11763,6 +11935,11 @@ def validate(root: Path) -> None: iwooos_projection_page, "{ key: 'dockerSystemd', rank: 'P1-1', value: '50%'", ) + assert_text_contains( + "iwooos_page.high_value_config_control_coverage_ssh_network_percent", + iwooos_projection_page, + "{ key: 'sshNetwork', rank: 'P1-2', value: '54%'", + ) assert_text_before( "iwooos_page.high_value_config_control_coverage_before_owner_packet", iwooos_projection_page, @@ -11775,7 +11952,7 @@ def validate(root: Path) -> None: "high_value_config_control_coverage_category_count=14", "high_value_config_control_coverage_c0_category_count=8", "high_value_config_control_coverage_c1_category_count=4", - "high_value_config_control_coverage_average_percent=64", + "high_value_config_control_coverage_average_percent=65", "high_value_config_control_coverage_needs_live_evidence_count=6", "high_value_config_control_coverage_owner_response_required_count=14", "high_value_config_control_coverage_owner_response_received_count=0", @@ -11785,10 +11962,22 @@ def validate(root: Path) -> None: "host_service_config_inventory_surface_count=9", "host_service_config_inventory_write_capable_surface_count=3", "host_service_config_inventory_runtime_gate_count=0", + "ssh_network_access_inventory_surface_count=16", + "ssh_network_access_inventory_write_capable_surface_count=6", + "ssh_network_access_inventory_runtime_gate_count=0", "docker_compose_action_authorized=false", "systemctl_action_authorized=false", "repair_bot_execution_authorized=false", "ansible_apply_authorized=false", + "ssh_read_authorized=false", + "ssh_write_authorized=false", + "sudo_action_authorized=false", + "firewall_change_authorized=false", + "network_policy_apply_authorized=false", + "nodeport_change_authorized=false", + "wireguard_change_authorized=false", + "known_hosts_patch_authorized=false", + "host_keyscan_authorized=false", "runtime_execution_authorized=false", "host_write_authorized=false", "nginx_reload_authorized=false", @@ -11835,6 +12024,16 @@ def validate(root: Path) -> None: list(web_messages_en["iwooos"]["highValueConfigControlCoverage"]["items"].keys()), key, ) + assert_text_contains( + "web_messages.zh-TW.iwooos.highValueConfigControlCoverage.items.sshNetwork.body", + web_messages_zh["iwooos"]["highValueConfigControlCoverage"]["items"]["sshNetwork"]["body"], + "16 個 SSH / network access surface", + ) + assert_text_contains( + "web_messages.en.iwooos.highValueConfigControlCoverage.items.sshNetwork.body", + web_messages_en["iwooos"]["highValueConfigControlCoverage"]["items"]["sshNetwork"]["body"], + "16 個 SSH / network access surface", + ) assert_text_contains( "iwooos_page.high_value_config_owner_packet_testid", iwooos_projection_page, diff --git a/scripts/security/ssh-network-access-inventory.py b/scripts/security/ssh-network-access-inventory.py new file mode 100644 index 000000000..468b29986 --- /dev/null +++ b/scripts/security/ssh-network-access-inventory.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +""" +IwoooS SSH / network access repo-only 清冊。 + +本工具只讀取已提交的 repo 檔案,整理 SSH target、known_hosts policy、 +sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 SSH action rule。 +它不 SSH、不 keyscan、不讀 live firewall、不套用 NetworkPolicy、不改 +NodePort、不啟動 WireGuard、不執行 sudo 或任何主機命令。 +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) + + +SURFACES: list[dict[str, Any]] = [ + { + "surface_id": "ansible_inventory_ssh_targets", + "label": "Ansible inventory SSH targets", + "source_path": "infra/ansible/inventory/hosts.yml", + "expected_scope": "110_111_112_120_121_188", + "config_kind": "ssh_target_inventory", + "control_tier": "C1", + "current_state": "repo_source_visible_needs_pinned_host_key_policy", + "access_scope": ["192.168.0.110", "192.168.0.111", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 host owner、pinned known_hosts disposition、ProxyJump policy、key owner 與 rollback owner。", + }, + { + "surface_id": "ansible_common_ssh_args", + "label": "Ansible common SSH host key policy", + "source_path": "infra/ansible/inventory/group_vars/all.yml", + "expected_scope": "multi_host", + "config_kind": "ssh_client_policy", + "control_tier": "C1", + "current_state": "accept_new_policy_visible_needs_owner_disposition", + "access_scope": ["StrictHostKeyChecking=accept-new", "ConnectTimeout=10"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "確認 accept-new 是否只限 bootstrap,補升級 pinned known_hosts 的 owner 與時間窗。", + }, + { + "surface_id": "gitea_cd_known_hosts_secret", + "label": "Gitea CD repair known_hosts secret", + "source_path": ".gitea/workflows/cd.yaml", + "expected_scope": "110_120_121_188_known_hosts", + "config_kind": "known_hosts_secret_workflow", + "control_tier": "C1", + "current_state": "repo_guard_visible_live_secret_not_verified", + "access_scope": ["192.168.0.110", "192.168.0.120", "192.168.0.121", "192.168.0.188"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 owner-provided known_hosts secret metadata、缺 120 時的處置、key rotation owner 與失敗通知 owner。", + }, + { + "surface_id": "gitea_cd_deploy_ssh", + "label": "Gitea CD K8s deploy SSH path", + "source_path": ".gitea/workflows/cd.yaml", + "expected_scope": "k8s_ssh_host", + "config_kind": "ci_deploy_ssh", + "control_tier": "C1", + "current_state": "write_capable_deploy_path_visible_gate_closed", + "access_scope": ["K8S_SSH_HOST", "deploy_key", "kubectl apply", "ArgoCD sync"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 deploy SSH host owner、maintenance window、rollback owner、post-check 指標與 break-glass policy。", + }, + { + "surface_id": "gitea_cd_dev_ssh", + "label": "Gitea CD dev deploy SSH path", + "source_path": ".gitea/workflows/cd-dev.yaml", + "expected_scope": "192.168.0.120", + "config_kind": "ci_deploy_ssh", + "control_tier": "C1", + "current_state": "dev_write_capable_deploy_path_visible_gate_closed", + "access_scope": ["192.168.0.120", "deploy_key", "kubectl apply"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "確認 dev deploy key scope、host key policy、rollback owner 與 dev/prod 邊界。", + }, + { + "surface_id": "deploy_alerts_ssh_path", + "label": "Deploy alerts SSH path", + "source_path": ".gitea/workflows/deploy-alerts.yaml", + "expected_scope": "192.168.0.110", + "config_kind": "ci_deploy_ssh", + "control_tier": "C1", + "current_state": "write_capable_alert_deploy_path_visible_gate_closed", + "access_scope": ["192.168.0.110", "deploy alert scripts"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 alert deploy owner、known_hosts pinning、rollback owner、post-check 與通知路徑。", + }, + { + "surface_id": "monitoring_discover_docker_ssh", + "label": "Monitoring Docker discovery SSH scanner", + "source_path": "ops/monitoring/discover_docker.py", + "expected_scope": "110_188_docker_hosts", + "config_kind": "ssh_discovery_script", + "control_tier": "C1", + "current_state": "accept_new_scanner_visible_needs_read_only_gate", + "access_scope": ["192.168.0.110", "192.168.0.188", "docker ps"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 scanner 執行 owner、read-only window、pinned known_hosts、輸出脫敏與失敗處置。", + }, + { + "surface_id": "monitoring_exporter_deploy_ssh", + "label": "Monitoring exporter deploy SSH script", + "source_path": "ops/monitoring/deploy-exporters.sh", + "expected_scope": "192.168.0.188", + "config_kind": "monitoring_ssh_deploy_script", + "control_tier": "C1", + "current_state": "write_capable_script_visible_gate_closed", + "access_scope": ["192.168.0.188", "scp", "docker compose up -d"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 exporter deploy owner、maintenance window、rollback owner、host key policy 與 post-check 指標。", + }, + { + "surface_id": "backup_config_ssh_capture", + "label": "Backup config SSH capture", + "source_path": "scripts/backup/backup-configs.sh", + "expected_scope": "110_188_120_121_cluster", + "config_kind": "ssh_backup_capture", + "control_tier": "C1", + "current_state": "read_capable_capture_visible_not_executed", + "access_scope": ["/etc/ssh", "/etc/nginx", "systemd", "docker", "k8s"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 backup execution owner、secret redaction proof、retention owner 與 restore validation。", + }, + { + "surface_id": "host_ops_sudoers_wrapper", + "label": "Host ops sudoers wrapper", + "source_path": "scripts/host-ops/awoooi-wrapper.sudoers", + "expected_scope": "host_ops_minimal_sudo", + "config_kind": "sudoers_policy", + "control_tier": "C1", + "current_state": "sudoers_source_visible_needs_live_owner_evidence", + "access_scope": ["awoooi-hosts-add", "docker kill SIGHUP", "promtool", "amtool"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 live sudoers hash、visudo validation、command owner、rollback owner 與 forbidden command proof。", + }, + { + "surface_id": "k8s_prod_network_policy", + "label": "K8s production NetworkPolicy", + "source_path": "k8s/awoooi-prod/02-network-policy.yaml", + "expected_scope": "awoooi_prod_namespace", + "config_kind": "k8s_network_policy", + "control_tier": "C1", + "current_state": "repo_policy_visible_needs_live_cluster_diff", + "access_scope": ["default deny", "ingress", "egress", "SSH egress", "Ollama", "monitoring"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 live NetworkPolicy diff、ingress / egress owner、rollback owner 與 route smoke。", + }, + { + "surface_id": "argocd_metrics_network_policy", + "label": "ArgoCD metrics NetworkPolicy", + "source_path": "k8s/argocd/argocd-metrics-network-policy.yaml", + "expected_scope": "argocd_namespace", + "config_kind": "k8s_network_policy", + "control_tier": "C1", + "current_state": "repo_policy_visible_needs_prometheus_owner_confirmation", + "access_scope": ["192.168.0.188", "argocd metrics", "192.168.0.0/24 UI"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 Prometheus scrape owner、NodePort exposure owner、live policy diff 與 rollback owner。", + }, + { + "surface_id": "argocd_metrics_nodeport", + "label": "ArgoCD metrics NodePort", + "source_path": "k8s/argocd/argocd-metrics-nodeport.yaml", + "expected_scope": "argocd_nodeport_30882_30883", + "config_kind": "k8s_nodeport_service", + "control_tier": "C1", + "current_state": "nodeport_source_visible_needs_exposure_review", + "access_scope": ["nodePort 30882", "nodePort 30883"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 NodePort exposure owner、firewall owner、Prometheus source whitelist 與 rollback owner。", + }, + { + "surface_id": "velero_metrics_nodeport", + "label": "Velero metrics NodePort", + "source_path": "k8s/velero/velero-metrics-service.yaml", + "expected_scope": "velero_nodeport_30885", + "config_kind": "k8s_nodeport_service", + "control_tier": "C1", + "current_state": "nodeport_source_visible_needs_exposure_review", + "access_scope": ["nodePort 30885", "backup metrics"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 Velero metrics exposure owner、firewall owner、Prometheus source whitelist 與 rollback owner。", + }, + { + "surface_id": "wireguard_mesh_runbook", + "label": "GCP Ollama WireGuard mesh runbook", + "source_path": "docs/runbooks/GCP-OLLAMA-WIREGUARD-MESH.md", + "expected_scope": "110_111_120_121_gcp_a_gcp_b", + "config_kind": "wireguard_runbook", + "control_tier": "C1", + "current_state": "target_architecture_documented_not_applied", + "access_scope": ["10.77.114.0/24", "51820/udp", "GCP-A", "GCP-B"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 WireGuard owner、public-key metadata、firewall rule owner、canary plan、rollback owner 與 cutover gate。", + }, + { + "surface_id": "alert_rules_ssh_actions", + "label": "Alert rules SSH action surface", + "source_path": "apps/api/alert_rules.yaml", + "expected_scope": "ssh_mcp_action_catalog", + "config_kind": "alert_ssh_action_rules", + "control_tier": "C1", + "current_state": "ssh_action_catalog_visible_gate_closed", + "access_scope": ["ssh_diagnose", "docker restart", "systemctl restart", "docker compose", "docker prune"], + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 action owner、read/write/admin 分級、approval gate、cooldown、rollback owner 與 post-check 指標。", + }, +] + + +FALSE_BOUNDARIES = { + "runtime_execution_authorized": False, + "host_write_authorized": False, + "ssh_read_authorized": False, + "ssh_write_authorized": False, + "sudo_action_authorized": False, + "firewall_change_authorized": False, + "network_policy_apply_authorized": False, + "nodeport_change_authorized": False, + "wireguard_change_authorized": False, + "known_hosts_patch_authorized": False, + "host_keyscan_authorized": False, + "live_host_read_authorized": False, + "secret_value_collection_allowed": False, + "ssh_key_collection_allowed": False, + "active_scan_authorized": False, + "action_buttons_allowed": False, +} + + +WRITE_CAPABLE_KINDS = { + "ci_deploy_ssh", + "monitoring_ssh_deploy_script", + "sudoers_policy", + "alert_ssh_action_rules", +} + + +def git_short_sha(root: Path) -> str: + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + except Exception: + return "unknown" + + +def file_metadata(root: Path, source_path: str) -> dict[str, Any]: + path = root / source_path + exists = path.exists() + if not exists: + return {"source_exists": False, "line_count": 0, "sha256": None} + content = path.read_bytes() + return { + "source_exists": True, + "line_count": len(content.decode("utf-8", errors="replace").splitlines()), + "sha256": hashlib.sha256(content).hexdigest(), + } + + +def build_surface(root: Path, surface: dict[str, Any]) -> dict[str, Any]: + metadata = file_metadata(root, surface["source_path"]) + return { + **surface, + **metadata, + "owner_response_received": False, + "owner_response_accepted": False, + "live_evidence_received": False, + "maintenance_window_accepted": False, + "rollback_owner_accepted": False, + "runtime_gate_open": False, + "action_buttons_allowed": False, + } + + +def build_report(root: Path, generated_at: str | None) -> dict[str, Any]: + report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") + surfaces = [build_surface(root, surface) for surface in SURFACES] + expected_scopes = sorted({surface["expected_scope"] for surface in surfaces}) + write_capable = [surface for surface in surfaces if surface["config_kind"] in WRITE_CAPABLE_KINDS] + ssh_sources = [surface for surface in surfaces if "ssh" in surface["config_kind"] or surface["config_kind"] in {"known_hosts_secret_workflow", "sudoers_policy"}] + network_policies = [surface for surface in surfaces if surface["config_kind"] == "k8s_network_policy"] + nodeports = [surface for surface in surfaces if surface["config_kind"] == "k8s_nodeport_service"] + wireguard = [surface for surface in surfaces if surface["config_kind"] == "wireguard_runbook"] + + return { + "schema_version": "ssh_network_access_inventory_v1", + "generated_at": report_time, + "git_commit": git_short_sha(root), + "status": "repo_only_inventory_ready", + "source_scope": "committed_repo_files_only", + "summary": { + "surface_count": len(surfaces), + "source_exists_count": sum(1 for surface in surfaces if surface["source_exists"]), + "expected_scope_count": len(expected_scopes), + "ssh_source_surface_count": len(ssh_sources), + "network_policy_surface_count": len(network_policies), + "nodeport_surface_count": len(nodeports), + "sudoers_surface_count": sum(1 for surface in surfaces if surface["config_kind"] == "sudoers_policy"), + "wireguard_surface_count": len(wireguard), + "write_capable_surface_count": len(write_capable), + "surfaces_requiring_owner_response_count": sum(1 for surface in surfaces if surface["requires_owner_response"]), + "surfaces_requiring_live_evidence_count": sum(1 for surface in surfaces if surface["requires_live_evidence"]), + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "live_evidence_received_count": 0, + "maintenance_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + "coverage_percent_after_inventory": 54, + "coverage_percent_before_inventory": 48, + }, + "execution_boundaries": FALSE_BOUNDARIES, + "expected_scopes": expected_scopes, + "access_surfaces": surfaces, + "write_capable_surfaces": [ + { + "surface_id": surface["surface_id"], + "label": surface["label"], + "config_kind": surface["config_kind"], + "expected_scope": surface["expected_scope"], + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + } + for surface in write_capable + ], + "next_collection_order": [ + "gitea_cd_known_hosts_secret", + "ansible_inventory_ssh_targets", + "host_ops_sudoers_wrapper", + "k8s_prod_network_policy", + "alert_rules_ssh_actions", + "argocd_metrics_nodeport", + "velero_metrics_nodeport", + "wireguard_mesh_runbook", + "monitoring_discover_docker_ssh", + "backup_config_ssh_capture", + ], + "operator_interpretation": [ + "這是 repo-only SSH / network access 清冊,不是 live host、firewall 或 cluster truth。", + "source_exists=true 只代表 repo 檔案存在;不代表 known_hosts、sudoers、NetworkPolicy、NodePort 或 WireGuard 已套用。", + "write-capable SSH / sudoers / alert action surface 可見代表需被管控,不代表 SSH、sudo、docker、systemctl 或 kubectl 已授權。", + "所有 live hash、pinned host key、maintenance window、rollback owner、firewall owner 與 post-check 指標都仍需 owner response。", + ], + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="產生 IwoooS SSH / network access repo-only 清冊") + parser.add_argument("--root", default=".", help="repo root") + parser.add_argument("--output", help="輸出 JSON 檔") + parser.add_argument("--generated-at", help="固定 generated_at") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + root = Path(args.root).resolve() + report = build_report(root, args.generated_at) + text = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + if args.output: + Path(args.output).write_text(text + "\n", encoding="utf-8") + else: + print(text) + print( + "SSH_NETWORK_ACCESS_INVENTORY_OK " + f"surfaces={report['summary']['surface_count']} " + f"ssh_sources={report['summary']['ssh_source_surface_count']} " + f"nodeports={report['summary']['nodeport_surface_count']} " + f"runtime_gate={report['summary']['runtime_gate_count']}", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())