From 118967cabc826f6fdbc19ca01e71b0c8e8f80b51 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 11 Jun 2026 21:41:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E6=96=B0=E5=A2=9E=E4=B8=BB?= =?UTF-8?q?=E6=A9=9F=E6=9C=8D=E5=8B=99=E9=85=8D=E7=BD=AE=E5=8F=AA=E8=AE=80?= =?UTF-8?q?=E6=B8=85=E5=86=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/messages/en.json | 2 +- apps/web/messages/zh-TW.json | 2 +- apps/web/src/app/[locale]/iwooos/page.tsx | 9 +- docs/LOGBOOK.md | 39 ++ ...st_service_config_inventory_v1.schema.json | 209 ++++++++++ .../iwooos_posture_projection_v1.schema.json | 95 +++++ .../HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md | 6 +- .../security/HOST-SERVICE-CONFIG-INVENTORY.md | 104 +++++ .../IWOOOS-CONFIG-CONTROL-INVENTORY.md | 6 + docs/security/IWOOOS-POSTURE-PROJECTION.md | 1 + ...alue-config-control-coverage.snapshot.json | 28 +- ...ost-service-config-inventory.snapshot.json | 363 ++++++++++++++++++ .../iwooos-posture-projection.snapshot.json | 22 ++ .../high-value-config-control-coverage.py | 11 +- .../security/host-service-config-inventory.py | 310 +++++++++++++++ .../security-mirror-progress-guard.py | 176 +++++++++ 16 files changed, 1362 insertions(+), 21 deletions(-) create mode 100644 docs/schemas/host_service_config_inventory_v1.schema.json create mode 100644 docs/security/HOST-SERVICE-CONFIG-INVENTORY.md create mode 100644 docs/security/host-service-config-inventory.snapshot.json create mode 100644 scripts/security/host-service-config-inventory.py diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index f5d90d92..681f9dc8 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -14824,7 +14824,7 @@ "items": { "dockerSystemd": { "title": "Docker / systemd 主機服務", - "body": "下一步補 110 / 188 compose、systemd、port / volume / env 只讀 inventory、restart window 與 rollback owner。" + "body": "repo-only 清冊已納入 9 個 surface;下一步仍需 110 / 188 live hash、restart window、rollback owner 與 post-check 指標。" }, "sshNetwork": { "title": "SSH / network / firewall", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index f5d90d92..681f9dc8 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -14824,7 +14824,7 @@ "items": { "dockerSystemd": { "title": "Docker / systemd 主機服務", - "body": "下一步補 110 / 188 compose、systemd、port / volume / env 只讀 inventory、restart window 與 rollback owner。" + "body": "repo-only 清冊已納入 9 個 surface;下一步仍需 110 / 188 live hash、restart window、rollback owner 與 post-check 指標。" }, "sshNetwork": { "title": "SSH / network / firewall", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 30e19e8b..9259de6f 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -2076,7 +2076,7 @@ const highValueConfigControlCoverageSummary = [ ] as const const highValueConfigControlCoverageItems: HighValueConfigControlCoverageItem[] = [ - { key: 'dockerSystemd', rank: 'P1-1', value: '42%', icon: Server, tone: 'warn' }, + { key: 'dockerSystemd', rank: 'P1-1', value: '50%', icon: Server, tone: 'warn' }, { key: 'sshNetwork', rank: 'P1-2', value: '48%', 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' }, @@ -2095,6 +2095,13 @@ const highValueConfigControlCoverageBoundaries = [ 'high_value_config_control_coverage_owner_response_accepted_count=0', 'high_value_config_control_coverage_runtime_gate_count=0', 'high_value_config_control_coverage_action_button_count=0', + 'host_service_config_inventory_surface_count=9', + 'host_service_config_inventory_write_capable_surface_count=3', + 'host_service_config_inventory_runtime_gate_count=0', + 'docker_compose_action_authorized=false', + 'systemctl_action_authorized=false', + 'repair_bot_execution_authorized=false', + 'ansible_apply_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 ba9902d7..6c0de797 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,42 @@ +## 2026-06-11|IwoooS P1-1 Docker / systemd repo-only 清冊 + +**背景**:統帥要求所有重要配置都要被資安控管,且 Nginx 之外的 Docker Compose、systemd、repair-bot、Ansible service role 與 host config backup 也不能落在鬆散口頭規範。本段延續「先建立框架、只讀證據、低摩擦流程,再階段性收攏」原則,只做 repo-only 清冊,不碰 live host。 + +**完成**: + +- 新增 `host_service_config_inventory_v1` 產生器、schema、snapshot 與人讀文件,納入 `9` 個 repo-only surface。 +- 清冊覆蓋 `5` 個 host scope:`local_dev_only`、`192.168.0.110`、`192.168.0.188`、`110_188_120_121_cluster`、`multi_host`。 +- Docker Compose / reference surface `5`、repair-bot whitelist `2`、systemd restart surface `1`、write-capable surface `3`。 +- 所有 surface 均固定 owner response required;owner response received / accepted、live evidence、restart window、rollback owner、runtime gate 與 action button 全部仍為 `0`。 +- 高價值配置覆蓋矩陣的 `docker_compose_systemd_host_config` 從 `42%` 推進到 `50%`;此進度只代表 repo-only 清冊完成,不代表主機已驗證、可重啟或可執行。 +- IwoooS posture projection snapshot / schema、`security-mirror-progress-guard.py`、高價值配置文件、配置控管總清冊與前端高價值配置卡已同步新口徑。 +- `/zh-TW/iwooos` 的 P1-1 卡由 `42%` 更新為 `50%`,並新增 `docker_compose_action_authorized=false`、`systemctl_action_authorized=false`、`repair_bot_execution_authorized=false`、`ansible_apply_authorized=false` 等前台邊界標記;仍無任何操作按鈕。 + +**本地驗證**: + +- `python3 scripts/security/host-service-config-inventory.py --root . --generated-at 2026-06-11T23:20:00+08:00 --output docs/security/host-service-config-inventory.snapshot.json`:`HOST_SERVICE_CONFIG_INVENTORY_OK surfaces=9 hosts=5 write_capable=3 runtime_gate=0`。 +- `python3 scripts/security/high-value-config-control-coverage.py --root . --generated-at 2026-06-11T23:21:00+08:00 --output docs/security/high-value-config-control-coverage.snapshot.json`:`HIGH_VALUE_CONFIG_CONTROL_COVERAGE_OK categories=14 c0=8 avg=64 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/host-service-config-inventory.py scripts/security/high-value-config-control-coverage.py scripts/security/security-mirror-progress-guard.py`:通過。 +- JSON parse:host-service 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=679`。 +- `git diff --check`:通過。 +- 目標敏感片語掃描:IwoooS page、`zh-TW.json`、`en.json` 與本輪 security docs 未命中 `工作視窗` / `內部對話` / `對話內容` / `批准!繼續` / `In app browser` / `My request for Codex`。 + +**完成度同步**: + +- P1-1 repo-only surface 註冊:`100%`。 +- source existence / SHA256 hash:`100%`。 +- Docker / systemd 高價值配置成熟度:`42% -> 50%`。 +- owner response 收件 / 接受:`0%`。 +- live evidence collection:`0%`。 +- restart / apply / repair-bot / Ansible gate:`0%`。 +- IwoooS 整體仍維持 `64%`;active runtime gate 仍 `0`。 + +**邊界**:本段未 SSH、未讀 live host、未執行 `docker compose`、未執行 `systemctl`、未跑 repair-bot、未跑 Ansible apply、未更新套件、未重啟、未 active scan、未收 secret value、未改主機與未新增任何前端執行按鈕。 + ## 2026-06-11|P2-403C 前端紅線語彙收斂 Hotfix **背景**:P2-403C 已完成 Redis Dry-run Gate 與正式部署驗證;正式治理頁再檢查時,service health failure-only 通知合約的 redaction 說明仍使用過於貼近內部工作流程的詞彙。這些文字不是實際內容外露,也沒有可點執行按鈕,但前端治理頁應只顯示產品化的抽象邊界。 diff --git a/docs/schemas/host_service_config_inventory_v1.schema.json b/docs/schemas/host_service_config_inventory_v1.schema.json new file mode 100644 index 00000000..26f9de2c --- /dev/null +++ b/docs/schemas/host_service_config_inventory_v1.schema.json @@ -0,0 +1,209 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.wooo.work/schemas/host_service_config_inventory_v1.schema.json", + "title": "IwoooS Docker / systemd / host service repo-only 清冊", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "generated_at", + "git_commit", + "status", + "source_scope", + "summary", + "execution_boundaries", + "expected_host_scopes", + "config_surfaces", + "write_capable_surfaces", + "next_collection_order", + "operator_interpretation" + ], + "properties": { + "schema_version": { + "const": "host_service_config_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_host_scope_count", + "docker_compose_source_count", + "host_repair_whitelist_count", + "systemd_restart_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", + "restart_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": 9 }, + "source_exists_count": { "const": 9 }, + "expected_host_scope_count": { "const": 5 }, + "docker_compose_source_count": { "const": 5 }, + "host_repair_whitelist_count": { "const": 2 }, + "systemd_restart_surface_count": { "const": 1 }, + "write_capable_surface_count": { "const": 3 }, + "surfaces_requiring_owner_response_count": { "const": 9 }, + "surfaces_requiring_live_evidence_count": { "const": 8 }, + "owner_response_received_count": { "const": 0 }, + "owner_response_accepted_count": { "const": 0 }, + "live_evidence_received_count": { "const": 0 }, + "restart_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": 50 }, + "coverage_percent_before_inventory": { "const": 42 } + } + }, + "execution_boundaries": { + "type": "object", + "additionalProperties": { "const": false }, + "required": [ + "runtime_execution_authorized", + "host_write_authorized", + "ssh_read_authorized", + "ssh_write_authorized", + "docker_compose_action_authorized", + "systemctl_action_authorized", + "service_restart_authorized", + "sudo_action_authorized", + "live_host_read_authorized", + "secret_value_collection_allowed", + "active_scan_authorized", + "repair_bot_execution_authorized", + "ansible_apply_authorized", + "action_buttons_allowed" + ] + }, + "expected_host_scopes": { + "type": "array", + "minItems": 5, + "items": { "type": "string" } + }, + "config_surfaces": { + "type": "array", + "minItems": 9, + "maxItems": 9, + "items": { + "$ref": "#/$defs/config_surface" + } + }, + "write_capable_surfaces": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "surface_id", + "label", + "config_kind", + "expected_host_scope", + "service_scope", + "required_gate" + ], + "properties": { + "surface_id": { "type": "string" }, + "label": { "type": "string" }, + "config_kind": { "type": "string" }, + "expected_host_scope": { "type": "string" }, + "service_scope": { + "type": "array", + "items": { "type": "string" } + }, + "required_gate": { + "const": "owner_response_plus_maintenance_window_plus_rollback_owner" + } + } + } + }, + "next_collection_order": { + "type": "array", + "minItems": 9, + "items": { "type": "string" } + }, + "operator_interpretation": { + "type": "array", + "items": { "type": "string" } + } + }, + "$defs": { + "config_surface": { + "type": "object", + "additionalProperties": false, + "required": [ + "surface_id", + "label", + "source_path", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "current_state", + "requires_live_evidence", + "requires_owner_response", + "next_owner_action", + "source_exists", + "line_count", + "sha256", + "owner_response_received", + "owner_response_accepted", + "live_evidence_received", + "restart_window_accepted", + "rollback_owner_accepted", + "runtime_gate_open", + "action_buttons_allowed" + ], + "properties": { + "surface_id": { "type": "string" }, + "label": { "type": "string" }, + "source_path": { "type": "string" }, + "expected_host_scope": { "type": "string" }, + "config_kind": { "type": "string" }, + "service_scope": { + "type": "array", + "items": { "type": "string" } + }, + "control_tier": { "const": "C1" }, + "current_state": { "type": "string" }, + "requires_live_evidence": { "type": "boolean" }, + "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 }, + "restart_window_accepted": { "const": false }, + "rollback_owner_accepted": { "const": false }, + "runtime_gate_open": { "const": false }, + "action_buttons_allowed": { "const": false } + } + } + } +} diff --git a/docs/schemas/iwooos_posture_projection_v1.schema.json b/docs/schemas/iwooos_posture_projection_v1.schema.json index 364eb266..7089df2f 100644 --- a/docs/schemas/iwooos_posture_projection_v1.schema.json +++ b/docs/schemas/iwooos_posture_projection_v1.schema.json @@ -176,6 +176,25 @@ "high_value_config_control_coverage_runtime_gate_count", "high_value_config_control_coverage_action_button_count", "high_value_config_control_coverage_lowest_category_count", + "host_service_config_inventory_first_layer", + "host_service_config_inventory_surface_count", + "host_service_config_inventory_source_exists_count", + "host_service_config_inventory_expected_host_scope_count", + "host_service_config_inventory_docker_compose_source_count", + "host_service_config_inventory_host_repair_whitelist_count", + "host_service_config_inventory_systemd_restart_surface_count", + "host_service_config_inventory_write_capable_surface_count", + "host_service_config_inventory_owner_response_required_count", + "host_service_config_inventory_owner_response_received_count", + "host_service_config_inventory_owner_response_accepted_count", + "host_service_config_inventory_live_evidence_required_count", + "host_service_config_inventory_live_evidence_received_count", + "host_service_config_inventory_restart_window_accepted_count", + "host_service_config_inventory_rollback_owner_accepted_count", + "host_service_config_inventory_runtime_gate_count", + "host_service_config_inventory_action_button_count", + "host_service_config_inventory_coverage_percent_before_inventory", + "host_service_config_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", @@ -524,6 +543,82 @@ "type": "integer", "const": 4 }, + "host_service_config_inventory_first_layer": { + "type": "boolean", + "const": true + }, + "host_service_config_inventory_surface_count": { + "type": "integer", + "const": 9 + }, + "host_service_config_inventory_source_exists_count": { + "type": "integer", + "const": 9 + }, + "host_service_config_inventory_expected_host_scope_count": { + "type": "integer", + "const": 5 + }, + "host_service_config_inventory_docker_compose_source_count": { + "type": "integer", + "const": 5 + }, + "host_service_config_inventory_host_repair_whitelist_count": { + "type": "integer", + "const": 2 + }, + "host_service_config_inventory_systemd_restart_surface_count": { + "type": "integer", + "const": 1 + }, + "host_service_config_inventory_write_capable_surface_count": { + "type": "integer", + "const": 3 + }, + "host_service_config_inventory_owner_response_required_count": { + "type": "integer", + "const": 9 + }, + "host_service_config_inventory_owner_response_received_count": { + "type": "integer", + "const": 0 + }, + "host_service_config_inventory_owner_response_accepted_count": { + "type": "integer", + "const": 0 + }, + "host_service_config_inventory_live_evidence_required_count": { + "type": "integer", + "const": 8 + }, + "host_service_config_inventory_live_evidence_received_count": { + "type": "integer", + "const": 0 + }, + "host_service_config_inventory_restart_window_accepted_count": { + "type": "integer", + "const": 0 + }, + "host_service_config_inventory_rollback_owner_accepted_count": { + "type": "integer", + "const": 0 + }, + "host_service_config_inventory_runtime_gate_count": { + "type": "integer", + "const": 0 + }, + "host_service_config_inventory_action_button_count": { + "type": "integer", + "const": 0 + }, + "host_service_config_inventory_coverage_percent_before_inventory": { + "type": "integer", + "const": 42 + }, + "host_service_config_inventory_coverage_percent_after_inventory": { + "type": "integer", + "const": 50 + }, "high_value_config_owner_packet_first_layer": { "type": "boolean", "const": true diff --git a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md index 885a4893..d012eacd 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md @@ -34,7 +34,7 @@ | 優先 | 類別 | 目前成熟度 | 下一步 | |------|------|------------|--------| -| P1-1 | Docker Compose / systemd / host service config | `42%` | 補 110 / 188 compose、systemd、port / volume / env 只讀 inventory、restart window、rollback owner | +| 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-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 | @@ -96,3 +96,7 @@ python3 scripts/security/high-value-config-control-coverage.py \ | owner response 收件 | `0%` | 尚未收到或接受任何 owner response | | live evidence collection | `0%` | 未 SSH、未 live probe、未 active scan | | runtime gate | `0%` | 未開啟任何執行期閘門 | + +## 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`。 diff --git a/docs/security/HOST-SERVICE-CONFIG-INVENTORY.md b/docs/security/HOST-SERVICE-CONFIG-INVENTORY.md new file mode 100644 index 00000000..12fa7017 --- /dev/null +++ b/docs/security/HOST-SERVICE-CONFIG-INVENTORY.md @@ -0,0 +1,104 @@ +# IwoooS Docker / systemd / 主機服務配置只讀清冊 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-11 | +| 狀態 | `repo_only_inventory_ready` | +| 工具 | `scripts/security/host-service-config-inventory.py` | +| Snapshot | `docs/security/host-service-config-inventory.snapshot.json` | +| Schema | `docs/schemas/host_service_config_inventory_v1.schema.json` | +| runtime gate | `0` | + +## 1. 目的 + +此清冊補齊高價值配置覆蓋矩陣中最低覆蓋的 `docker_compose_systemd_host_config`。本階段只從已提交 repo 檔案整理 Docker Compose、systemd / repair bot 白名單、Ansible service role 與 config backup coverage,不讀 live host,也不執行任何服務操作。 + +此清冊不是 host truth,也不是重啟批准;它只讓 P1-1 從「尚需 inventory」推進到「repo-only inventory ready」。 + +## 2. 覆蓋摘要 + +| 指標 | 目前值 | 說明 | +|------|--------|------| +| repo surface | `9` | 全部來源檔案存在 | +| host scope | `5` | `local_dev_only`、`192.168.0.110`、`192.168.0.188`、`110_188_120_121_cluster`、`multi_host` | +| Docker Compose / reference | `5` | local dev、110 monitoring、188 exporters、110 Sentry reference、110 Langfuse | +| host repair whitelist | `2` | 110 / 188 repair-bot | +| systemd restart surface | `1` | 188 repair-bot 內的 redis / nginx / ollama restart 白名單 | +| write-capable surface | `3` | Ansible docker compose role、110 repair-bot、188 repair-bot | +| owner response required | `9` | 每個 surface 都需要 owner response | +| live evidence required | `8` | local dev compose 之外仍需 owner-provided live hash / disposition | +| owner response received / accepted | `0 / 0` | 不得假性提高 | +| live evidence received | `0` | 不 SSH、不讀 live host | +| restart window / rollback owner accepted | `0 / 0` | 不得重啟 | +| runtime gate / action button | `0 / 0` | 不提供操作入口 | +| Docker/systemd 類別成熟度 | `42% -> 50%` | 只代表 repo-only 清冊完成,不代表 runtime 可執行 | + +## 3. 已納入 surface + +| Surface | Host scope | 類型 | 下一步 | +|---------|------------|------|--------| +| `local_dev_compose` | `local_dev_only` | local dev compose | 確認不得作 production compose,補 dev secret placeholder policy | +| `monitoring_110_compose` | `192.168.0.110` | Docker Compose | 補 live compose hash、restart window、rollback owner、post-check 指標 | +| `monitoring_exporters_188_compose` | `192.168.0.188` | Docker Compose | 補 live compose hash、env source policy、restart window、rollback owner | +| `sentry_110_reference_compose` | `192.168.0.110` | reference compose | 確認實際 source-of-truth、official revision、backup path、rollback owner | +| `langfuse_110_compose` | `192.168.0.110` | Docker Compose | 補 live compose hash、secret placeholder disposition、restart window、rollback owner | +| `ansible_docker_compose_service_role` | `multi_host` | Ansible executor role | 補使用範圍、allowed service_dir、check-mode、rollback owner、人工 gate | +| `repair_bot_110_whitelist` | `192.168.0.110` | repair whitelist | 補 authorized_keys binding、disable switch、audit log path、rollback owner、post-check | +| `repair_bot_188_whitelist` | `192.168.0.188` | repair whitelist | 補 systemd restart approval gate、sudoers boundary、disable switch、rollback owner、route smoke | +| `config_backup_host_capture` | `110_188_120_121_cluster` | config backup capture | 補 latest backup status、restore drill owner、secret handling proof、retention owner | + +## 4. 固定 0 / false 邊界 + +以下旗標必須維持 `false`: + +```text +runtime_execution_authorized=false +host_write_authorized=false +ssh_read_authorized=false +ssh_write_authorized=false +docker_compose_action_authorized=false +systemctl_action_authorized=false +service_restart_authorized=false +sudo_action_authorized=false +live_host_read_authorized=false +secret_value_collection_allowed=false +active_scan_authorized=false +repair_bot_execution_authorized=false +ansible_apply_authorized=false +action_buttons_allowed=false +``` + +## 5. 判讀規則 + +1. `source_exists=true` 只代表 repo 內有檔案,不代表 live host 與 repo 一致。 +2. `sha256` 是 repo file hash,不是 live file hash。 +3. repair-bot 與 Ansible role 可見代表「需被管控」,不是可使用。 +4. `docker compose up -d`、`systemctl restart`、`sudo`、repair-bot、Ansible apply 都必須等待 owner response、maintenance window、rollback owner 與 post-check 指標。 +5. 此清冊不得收集 secret value;若需要 secret parity,只能收 secret name / owner / injection metadata。 + +## 6. 指令 + +```bash +python3 scripts/security/host-service-config-inventory.py \ + --root . \ + --output docs/security/host-service-config-inventory.snapshot.json +``` + +固定 committed snapshot 時間: + +```bash +python3 scripts/security/host-service-config-inventory.py \ + --root . \ + --generated-at 2026-06-11T22:40:00+08:00 \ + --output docs/security/host-service-config-inventory.snapshot.json +``` + +## 7. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| repo-only surface 註冊 | `100%` | 9 個 surface 全部納入 snapshot | +| source existence / hash | `100%` | 只讀 SHA256 與 line count 已固定 | +| owner response 收件 | `0%` | 尚未收到或接受任何 owner response | +| live evidence collection | `0%` | 未 SSH、未讀 live host、未 active scan | +| restart / apply gate | `0%` | 未開啟 docker compose / systemctl / Ansible / repair-bot 操作 | diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index 7f6c1181..32e4b206 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -39,6 +39,12 @@ 最低覆蓋優先順序為 Docker Compose / systemd、SSH / network、backup / restore、monitoring / alerting。這些是下一波 P1 只讀 inventory 的優先順序,不代表可以 restart、reload、scan 或收 secret value。 +### 0.2 2026-06-11 Docker / systemd repo-only 清冊 + +`host_service_config_inventory_v1` 已把 Docker Compose、systemd / repair-bot、Ansible service role 與 host config backup capture 納入只讀 snapshot。清冊目前共有 `9` 個 surface、`5` 個 host scope、`3` 個 write-capable surface、`2` 個 repair-bot whitelist 與 `1` 個 systemd restart surface,讓 Docker / systemd 類別成熟度從 `42%` 推進到 `50%`。 + +此更新仍不是 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 讀寫。 + ## 1. 目前已不符合新要求的項目 | 優先 | 項目 | 現況 | 風險 | 本階段處置 | diff --git a/docs/security/IWOOOS-POSTURE-PROJECTION.md b/docs/security/IWOOOS-POSTURE-PROJECTION.md index 54e50f7c..fa088529 100644 --- a/docs/security/IWOOOS-POSTURE-PROJECTION.md +++ b/docs/security/IWOOOS-POSTURE-PROJECTION.md @@ -90,6 +90,7 @@ IwoooS 首版只讀取或對齊以下已提交 evidence: 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 或主機操作授權。 +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 已授權。 ## 3.1 既有前端資安頁面整合 diff --git a/docs/security/high-value-config-control-coverage.snapshot.json b/docs/security/high-value-config-control-coverage.snapshot.json index 188422f6..1a8e13fa 100644 --- a/docs/security/high-value-config-control-coverage.snapshot.json +++ b/docs/security/high-value-config-control-coverage.snapshot.json @@ -327,15 +327,17 @@ "action_buttons_allowed": false, "category_id": "docker_compose_systemd_host_config", "control_tier": "C1", - "coverage_percent": 42, - "coverage_status": "inventory_needed", - "current_gap": "110 / 188 Docker Compose、systemd、port / volume / env 差異仍需只讀 inventory。", + "coverage_percent": 50, + "coverage_status": "repo_only_inventory_ready_needs_live_owner_evidence", + "current_gap": "repo-only 清冊已納入 9 個 surface;仍缺 110 / 188 live hash、restart window、rollback owner 與 post-check 指標。", "evidence_refs": [ "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md", + "docs/security/HOST-SERVICE-CONFIG-INVENTORY.md", + "docs/security/host-service-config-inventory.snapshot.json", "docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md" ], "label": "Docker Compose / systemd / host service config", - "next_owner_action": "補 compose / systemd owner、restart window、rollback owner 與 post-check 指標。", + "next_owner_action": "補 owner-provided live hash / disposition、compose / systemd owner、restart window、rollback owner 與 post-check 指標。", "owner_response_accepted": false, "owner_response_received": false, "owner_response_required": true, @@ -518,16 +520,9 @@ "secret_value_collection_allowed": false, "workflow_modification_authorized": false }, - "generated_at": "2026-06-11T21:30:00+08:00", - "git_commit": "d448ae36", + "generated_at": "2026-06-11T23:21:00+08:00", + "git_commit": "0a82648e", "lowest_coverage_categories": [ - { - "category_id": "docker_compose_systemd_host_config", - "coverage_percent": 42, - "current_gap": "110 / 188 Docker Compose、systemd、port / volume / env 差異仍需只讀 inventory。", - "label": "Docker Compose / systemd / host service config", - "next_owner_action": "補 compose / systemd owner、restart window、rollback owner 與 post-check 指標。" - }, { "category_id": "ssh_firewall_network_access", "coverage_percent": 48, @@ -535,6 +530,13 @@ "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, + "current_gap": "repo-only 清冊已納入 9 個 surface;仍缺 110 / 188 live hash、restart window、rollback owner 與 post-check 指標。", + "label": "Docker Compose / systemd / host service config", + "next_owner_action": "補 owner-provided live hash / disposition、compose / systemd owner、restart window、rollback owner 與 post-check 指標。" + }, { "category_id": "backup_restore_credential", "coverage_percent": 52, diff --git a/docs/security/host-service-config-inventory.snapshot.json b/docs/security/host-service-config-inventory.snapshot.json new file mode 100644 index 00000000..7bb61f53 --- /dev/null +++ b/docs/security/host-service-config-inventory.snapshot.json @@ -0,0 +1,363 @@ +{ + "config_surfaces": [ + { + "action_buttons_allowed": false, + "config_kind": "docker_compose_source", + "control_tier": "C1", + "current_state": "repo_source_visible", + "expected_host_scope": "local_dev_only", + "label": "AWOOOI local development compose", + "line_count": 137, + "live_evidence_received": false, + "next_owner_action": "確認本檔僅供 local dev,不得作為 production compose;補 dev secret placeholder policy。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": false, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "web", + "api", + "postgres", + "redis" + ], + "sha256": "4a27bcde139b5aef6a9f3080187af5bec73d1efd9c09ed2752b0baaa5f507024", + "source_exists": true, + "source_path": "docker-compose.yml", + "surface_id": "local_dev_compose" + }, + { + "action_buttons_allowed": false, + "config_kind": "docker_compose_source", + "control_tier": "C1", + "current_state": "repo_source_visible_with_live_drift_warning", + "expected_host_scope": "192.168.0.110", + "label": "110 monitoring docker compose", + "line_count": 148, + "live_evidence_received": false, + "next_owner_action": "補 110 live compose hash、restart window、rollback owner、post-check 指標與 drift disposition。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "cadvisor", + "prometheus", + "grafana", + "blackbox-exporter", + "alertmanager", + "github-exporter" + ], + "sha256": "00126e9a5cb7a3cf2bf02cfddefea11f05849b46835a4e602eac4777fcb25281", + "source_exists": true, + "source_path": "k8s/monitoring/docker-compose-110.yml", + "surface_id": "monitoring_110_compose" + }, + { + "action_buttons_allowed": false, + "config_kind": "docker_compose_source", + "control_tier": "C1", + "current_state": "repo_source_visible_needs_live_hash", + "expected_host_scope": "192.168.0.188", + "label": "188 database exporters compose", + "line_count": 69, + "live_evidence_received": false, + "next_owner_action": "補 188 exporter compose live hash、env source policy、restart window 與 rollback owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "postgres-exporter", + "redis-exporter" + ], + "sha256": "3ffb3bd2e98091d18e60b74721904777c27f279c37ab6e873b82e6ef73eb87d4", + "source_exists": true, + "source_path": "ops/monitoring/docker-compose.exporters.yaml", + "surface_id": "monitoring_exporters_188_compose" + }, + { + "action_buttons_allowed": false, + "config_kind": "docker_compose_reference", + "control_tier": "C1", + "current_state": "reference_only_not_runtime_source", + "expected_host_scope": "192.168.0.110", + "label": "110 Sentry self-hosted reference compose", + "line_count": 49, + "live_evidence_received": false, + "next_owner_action": "確認 110 Sentry 實際 source-of-truth、official self-hosted revision、backup path 與 rollback owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "sentry-placeholder-reference" + ], + "sha256": "bba852dc0d73934998fa375130168615f9ac7611ce3f3efaa901e3b7e222eae3", + "source_exists": true, + "source_path": "ops/sentry-self-hosted/docker-compose.yml", + "surface_id": "sentry_110_reference_compose" + }, + { + "action_buttons_allowed": false, + "config_kind": "docker_compose_source", + "control_tier": "C1", + "current_state": "repo_source_visible_needs_secret_policy_review", + "expected_host_scope": "192.168.0.110", + "label": "110 Langfuse compose", + "line_count": 71, + "live_evidence_received": false, + "next_owner_action": "補 110 live compose hash、secret placeholder disposition、restart window 與 rollback owner。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "langfuse", + "langfuse-db" + ], + "sha256": "6c703a27525e62ef4d4d3c4cba8a89d64f646b01020782e35d22a3bf73f2dc83", + "source_exists": true, + "source_path": "infra/langfuse/docker-compose.yml", + "surface_id": "langfuse_110_compose" + }, + { + "action_buttons_allowed": false, + "config_kind": "ansible_service_executor", + "control_tier": "C1", + "current_state": "executor_role_visible_needs_gate_mapping", + "expected_host_scope": "multi_host", + "label": "Ansible docker-compose-service role", + "line_count": 18, + "live_evidence_received": false, + "next_owner_action": "補 role 使用範圍、allowed service_dir、check-mode plan、rollback owner 與人工批准 gate。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "docker compose up -d" + ], + "sha256": "cee214a8651f46c2d8be05054dddadc243a26bff51a64bd9cf42dd2ec0b7b1b3", + "source_exists": true, + "source_path": "infra/ansible/roles/docker-compose-service/tasks/main.yml", + "surface_id": "ansible_docker_compose_service_role" + }, + { + "action_buttons_allowed": false, + "config_kind": "host_repair_whitelist", + "control_tier": "C1", + "current_state": "write_capable_whitelist_visible_gate_closed", + "expected_host_scope": "192.168.0.110", + "label": "110 repair-bot compose whitelist", + "line_count": 67, + "live_evidence_received": false, + "next_owner_action": "補 authorized_keys command binding、disable switch、audit log path、rollback owner 與 post-check 指標。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "sentry", + "harbor", + "gitea", + "gitea-runner", + "langfuse", + "alertmanager", + "signoz" + ], + "sha256": "093d4f85c398806dee62c2831fa4fe7e1f8fddca6e3cfcc9dbe4d5e0d66cdf3b", + "source_exists": true, + "source_path": "scripts/repair-bot/repair-bot-110.sh", + "surface_id": "repair_bot_110_whitelist" + }, + { + "action_buttons_allowed": false, + "config_kind": "host_repair_whitelist", + "control_tier": "C1", + "current_state": "write_capable_whitelist_visible_gate_closed", + "expected_host_scope": "192.168.0.188", + "label": "188 repair-bot compose/systemd whitelist", + "line_count": 85, + "live_evidence_received": false, + "next_owner_action": "補 systemd restart approval gate、sudoers boundary、disable switch、rollback owner 與 route smoke。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "openclaw", + "minio", + "signoz", + "redis", + "nginx", + "ollama" + ], + "sha256": "fb2eb786d04edbf5d5be581a53bbe188ac66f0895aa016328b031c72f6182918", + "source_exists": true, + "source_path": "scripts/repair-bot/repair-bot-188.sh", + "surface_id": "repair_bot_188_whitelist" + }, + { + "action_buttons_allowed": false, + "config_kind": "backup_capture_contract", + "control_tier": "C1", + "current_state": "capture_script_visible_not_executed_by_this_inventory", + "expected_host_scope": "110_188_120_121_cluster", + "label": "host config backup capture contract", + "line_count": 359, + "live_evidence_received": false, + "next_owner_action": "補 latest backup status、restore drill owner、secret handling proof、retention owner 與 restore validation plan。", + "owner_response_accepted": false, + "owner_response_received": false, + "requires_live_evidence": true, + "requires_owner_response": true, + "restart_window_accepted": false, + "rollback_owner_accepted": false, + "runtime_gate_open": false, + "service_scope": [ + "systemd", + "docker", + "nginx", + "cron", + "k8s", + "host-configs" + ], + "sha256": "d24301cff44e464bd19ce0792362be16916ccde8c92f92351a19ef4ee988f15e", + "source_exists": true, + "source_path": "scripts/backup/backup-configs.sh", + "surface_id": "config_backup_host_capture" + } + ], + "execution_boundaries": { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "ansible_apply_authorized": false, + "docker_compose_action_authorized": false, + "host_write_authorized": false, + "live_host_read_authorized": false, + "repair_bot_execution_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "service_restart_authorized": false, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "sudo_action_authorized": false, + "systemctl_action_authorized": false + }, + "expected_host_scopes": [ + "110_188_120_121_cluster", + "192.168.0.110", + "192.168.0.188", + "local_dev_only", + "multi_host" + ], + "generated_at": "2026-06-11T23:20:00+08:00", + "git_commit": "0a82648e", + "next_collection_order": [ + "repair_bot_110_whitelist", + "repair_bot_188_whitelist", + "monitoring_110_compose", + "monitoring_exporters_188_compose", + "langfuse_110_compose", + "config_backup_host_capture", + "ansible_docker_compose_service_role", + "sentry_110_reference_compose", + "local_dev_compose" + ], + "operator_interpretation": [ + "這是 repo-only 主機服務配置清冊,不是 live host 盤點。", + "write-capable 白名單與 Ansible role 可見,不代表 repair-bot、docker compose、systemctl 或 sudo 已授權。", + "所有 live hash、restart window、rollback owner、post-check 指標都仍需 owner response。", + "本清冊讓 Docker/systemd 類別從 inventory_needed 進到 repo_only_inventory_ready,但 runtime gate 仍為 0。" + ], + "schema_version": "host_service_config_inventory_v1", + "source_scope": "committed_repo_files_only", + "status": "repo_only_inventory_ready", + "summary": { + "action_button_count": 0, + "coverage_percent_after_inventory": 50, + "coverage_percent_before_inventory": 42, + "docker_compose_source_count": 5, + "expected_host_scope_count": 5, + "host_repair_whitelist_count": 2, + "live_evidence_received_count": 0, + "owner_response_accepted_count": 0, + "owner_response_received_count": 0, + "restart_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "source_exists_count": 9, + "surface_count": 9, + "surfaces_requiring_live_evidence_count": 8, + "surfaces_requiring_owner_response_count": 9, + "systemd_restart_surface_count": 1, + "write_capable_surface_count": 3 + }, + "write_capable_surfaces": [ + { + "config_kind": "ansible_service_executor", + "expected_host_scope": "multi_host", + "label": "Ansible docker-compose-service role", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "service_scope": [ + "docker compose up -d" + ], + "surface_id": "ansible_docker_compose_service_role" + }, + { + "config_kind": "host_repair_whitelist", + "expected_host_scope": "192.168.0.110", + "label": "110 repair-bot compose whitelist", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "service_scope": [ + "sentry", + "harbor", + "gitea", + "gitea-runner", + "langfuse", + "alertmanager", + "signoz" + ], + "surface_id": "repair_bot_110_whitelist" + }, + { + "config_kind": "host_repair_whitelist", + "expected_host_scope": "192.168.0.188", + "label": "188 repair-bot compose/systemd whitelist", + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + "service_scope": [ + "openclaw", + "minio", + "signoz", + "redis", + "nginx", + "ollama" + ], + "surface_id": "repair_bot_188_whitelist" + } + ] +} diff --git a/docs/security/iwooos-posture-projection.snapshot.json b/docs/security/iwooos-posture-projection.snapshot.json index 76584a29..572e5ff3 100644 --- a/docs/security/iwooos-posture-projection.snapshot.json +++ b/docs/security/iwooos-posture-projection.snapshot.json @@ -25,6 +25,9 @@ "docs/security/domain-tls-certbot-inventory.snapshot.json", "docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md", "docs/schemas/domain_tls_certbot_inventory_v1.schema.json", + "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/LOGBOOK.md", "docs/workplans/2026-06-04-iwooos-security-governance-p0.md", "apps/web/src/app/[locale]/iwooos/page.tsx", @@ -241,6 +244,25 @@ "high_value_config_control_coverage_runtime_gate_count": 0, "high_value_config_control_coverage_action_button_count": 0, "high_value_config_control_coverage_lowest_category_count": 4, + "host_service_config_inventory_first_layer": true, + "host_service_config_inventory_surface_count": 9, + "host_service_config_inventory_source_exists_count": 9, + "host_service_config_inventory_expected_host_scope_count": 5, + "host_service_config_inventory_docker_compose_source_count": 5, + "host_service_config_inventory_host_repair_whitelist_count": 2, + "host_service_config_inventory_systemd_restart_surface_count": 1, + "host_service_config_inventory_write_capable_surface_count": 3, + "host_service_config_inventory_owner_response_required_count": 9, + "host_service_config_inventory_owner_response_received_count": 0, + "host_service_config_inventory_owner_response_accepted_count": 0, + "host_service_config_inventory_live_evidence_required_count": 8, + "host_service_config_inventory_live_evidence_received_count": 0, + "host_service_config_inventory_restart_window_accepted_count": 0, + "host_service_config_inventory_rollback_owner_accepted_count": 0, + "host_service_config_inventory_runtime_gate_count": 0, + "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, "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/scripts/security/high-value-config-control-coverage.py b/scripts/security/high-value-config-control-coverage.py index a60f4437..be9b97cb 100644 --- a/scripts/security/high-value-config-control-coverage.py +++ b/scripts/security/high-value-config-control-coverage.py @@ -119,14 +119,16 @@ CONTROL_STATUS_BY_CATEGORY = { "next_owner_action": "補 rule diff、receiver diff、reload owner、failure-only notification policy 與 route smoke。", }, "docker_compose_systemd_host_config": { - "coverage_status": "inventory_needed", - "coverage_percent": 42, + "coverage_status": "repo_only_inventory_ready_needs_live_owner_evidence", + "coverage_percent": 50, "evidence_refs": [ "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md", + "docs/security/HOST-SERVICE-CONFIG-INVENTORY.md", + "docs/security/host-service-config-inventory.snapshot.json", "docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md", ], - "current_gap": "110 / 188 Docker Compose、systemd、port / volume / env 差異仍需只讀 inventory。", - "next_owner_action": "補 compose / systemd owner、restart window、rollback owner 與 post-check 指標。", + "current_gap": "repo-only 清冊已納入 9 個 surface;仍缺 110 / 188 live hash、restart window、rollback owner 與 post-check 指標。", + "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", @@ -254,6 +256,7 @@ def build_report(root: Path, generated_at: str | None) -> dict[str, Any]: "policy_defined_needs_restore_drill_owner", "policy_ready_needs_drift_evidence", "inventory_needed", + "repo_only_inventory_ready_needs_live_owner_evidence", "policy_ready_needs_network_matrix", "policy_ready_needs_dry_run_pack", } diff --git a/scripts/security/host-service-config-inventory.py b/scripts/security/host-service-config-inventory.py new file mode 100644 index 00000000..7d50bd31 --- /dev/null +++ b/scripts/security/host-service-config-inventory.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +""" +IwoooS Docker / systemd / host service repo-only 清冊。 + +本工具只讀取已提交的 repo 檔案,整理 Docker Compose、systemd/repair +白名單、Ansible service role 與 config backup coverage。它不 SSH、不讀 +live host、不執行 docker compose、不執行 systemctl、不重啟任何服務。 +""" + +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": "local_dev_compose", + "label": "AWOOOI local development compose", + "source_path": "docker-compose.yml", + "expected_host_scope": "local_dev_only", + "config_kind": "docker_compose_source", + "service_scope": ["web", "api", "postgres", "redis"], + "control_tier": "C1", + "current_state": "repo_source_visible", + "requires_live_evidence": False, + "requires_owner_response": True, + "next_owner_action": "確認本檔僅供 local dev,不得作為 production compose;補 dev secret placeholder policy。", + }, + { + "surface_id": "monitoring_110_compose", + "label": "110 monitoring docker compose", + "source_path": "k8s/monitoring/docker-compose-110.yml", + "expected_host_scope": "192.168.0.110", + "config_kind": "docker_compose_source", + "service_scope": ["cadvisor", "prometheus", "grafana", "blackbox-exporter", "alertmanager", "github-exporter"], + "control_tier": "C1", + "current_state": "repo_source_visible_with_live_drift_warning", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 110 live compose hash、restart window、rollback owner、post-check 指標與 drift disposition。", + }, + { + "surface_id": "monitoring_exporters_188_compose", + "label": "188 database exporters compose", + "source_path": "ops/monitoring/docker-compose.exporters.yaml", + "expected_host_scope": "192.168.0.188", + "config_kind": "docker_compose_source", + "service_scope": ["postgres-exporter", "redis-exporter"], + "control_tier": "C1", + "current_state": "repo_source_visible_needs_live_hash", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 188 exporter compose live hash、env source policy、restart window 與 rollback owner。", + }, + { + "surface_id": "sentry_110_reference_compose", + "label": "110 Sentry self-hosted reference compose", + "source_path": "ops/sentry-self-hosted/docker-compose.yml", + "expected_host_scope": "192.168.0.110", + "config_kind": "docker_compose_reference", + "service_scope": ["sentry-placeholder-reference"], + "control_tier": "C1", + "current_state": "reference_only_not_runtime_source", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "確認 110 Sentry 實際 source-of-truth、official self-hosted revision、backup path 與 rollback owner。", + }, + { + "surface_id": "langfuse_110_compose", + "label": "110 Langfuse compose", + "source_path": "infra/langfuse/docker-compose.yml", + "expected_host_scope": "192.168.0.110", + "config_kind": "docker_compose_source", + "service_scope": ["langfuse", "langfuse-db"], + "control_tier": "C1", + "current_state": "repo_source_visible_needs_secret_policy_review", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 110 live compose hash、secret placeholder disposition、restart window 與 rollback owner。", + }, + { + "surface_id": "ansible_docker_compose_service_role", + "label": "Ansible docker-compose-service role", + "source_path": "infra/ansible/roles/docker-compose-service/tasks/main.yml", + "expected_host_scope": "multi_host", + "config_kind": "ansible_service_executor", + "service_scope": ["docker compose up -d"], + "control_tier": "C1", + "current_state": "executor_role_visible_needs_gate_mapping", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 role 使用範圍、allowed service_dir、check-mode plan、rollback owner 與人工批准 gate。", + }, + { + "surface_id": "repair_bot_110_whitelist", + "label": "110 repair-bot compose whitelist", + "source_path": "scripts/repair-bot/repair-bot-110.sh", + "expected_host_scope": "192.168.0.110", + "config_kind": "host_repair_whitelist", + "service_scope": ["sentry", "harbor", "gitea", "gitea-runner", "langfuse", "alertmanager", "signoz"], + "control_tier": "C1", + "current_state": "write_capable_whitelist_visible_gate_closed", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 authorized_keys command binding、disable switch、audit log path、rollback owner 與 post-check 指標。", + }, + { + "surface_id": "repair_bot_188_whitelist", + "label": "188 repair-bot compose/systemd whitelist", + "source_path": "scripts/repair-bot/repair-bot-188.sh", + "expected_host_scope": "192.168.0.188", + "config_kind": "host_repair_whitelist", + "service_scope": ["openclaw", "minio", "signoz", "redis", "nginx", "ollama"], + "control_tier": "C1", + "current_state": "write_capable_whitelist_visible_gate_closed", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 systemd restart approval gate、sudoers boundary、disable switch、rollback owner 與 route smoke。", + }, + { + "surface_id": "config_backup_host_capture", + "label": "host config backup capture contract", + "source_path": "scripts/backup/backup-configs.sh", + "expected_host_scope": "110_188_120_121_cluster", + "config_kind": "backup_capture_contract", + "service_scope": ["systemd", "docker", "nginx", "cron", "k8s", "host-configs"], + "control_tier": "C1", + "current_state": "capture_script_visible_not_executed_by_this_inventory", + "requires_live_evidence": True, + "requires_owner_response": True, + "next_owner_action": "補 latest backup status、restore drill owner、secret handling proof、retention owner 與 restore validation plan。", + }, +] + + +FALSE_BOUNDARIES = { + "runtime_execution_authorized": False, + "host_write_authorized": False, + "ssh_read_authorized": False, + "ssh_write_authorized": False, + "docker_compose_action_authorized": False, + "systemctl_action_authorized": False, + "service_restart_authorized": False, + "sudo_action_authorized": False, + "live_host_read_authorized": False, + "secret_value_collection_allowed": False, + "active_scan_authorized": False, + "repair_bot_execution_authorized": False, + "ansible_apply_authorized": False, + "action_buttons_allowed": False, +} + + +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, + "restart_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_hosts = sorted({surface["expected_host_scope"] for surface in surfaces}) + write_capable = [ + surface + for surface in surfaces + if surface["config_kind"] in {"host_repair_whitelist", "ansible_service_executor"} + ] + live_evidence = [surface for surface in surfaces if surface["requires_live_evidence"]] + + return { + "schema_version": "host_service_config_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_host_scope_count": len(expected_hosts), + "docker_compose_source_count": sum( + 1 for surface in surfaces if surface["config_kind"] in {"docker_compose_source", "docker_compose_reference"} + ), + "host_repair_whitelist_count": sum(1 for surface in surfaces if surface["config_kind"] == "host_repair_whitelist"), + "systemd_restart_surface_count": 1, + "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": len(live_evidence), + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "live_evidence_received_count": 0, + "restart_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + "coverage_percent_after_inventory": 50, + "coverage_percent_before_inventory": 42, + }, + "execution_boundaries": FALSE_BOUNDARIES, + "expected_host_scopes": expected_hosts, + "config_surfaces": surfaces, + "write_capable_surfaces": [ + { + "surface_id": surface["surface_id"], + "label": surface["label"], + "config_kind": surface["config_kind"], + "expected_host_scope": surface["expected_host_scope"], + "service_scope": surface["service_scope"], + "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner", + } + for surface in write_capable + ], + "next_collection_order": [ + "repair_bot_110_whitelist", + "repair_bot_188_whitelist", + "monitoring_110_compose", + "monitoring_exporters_188_compose", + "langfuse_110_compose", + "config_backup_host_capture", + "ansible_docker_compose_service_role", + "sentry_110_reference_compose", + "local_dev_compose", + ], + "operator_interpretation": [ + "這是 repo-only 主機服務配置清冊,不是 live host 盤點。", + "write-capable 白名單與 Ansible role 可見,不代表 repair-bot、docker compose、systemctl 或 sudo 已授權。", + "所有 live hash、restart window、rollback owner、post-check 指標都仍需 owner response。", + "本清冊讓 Docker/systemd 類別從 inventory_needed 進到 repo_only_inventory_ready,但 runtime gate 仍為 0。", + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS host service repo-only config inventory") + parser.add_argument("--root", default=".", help="repo root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(root, args.generated_at) + payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(payload + "\n", encoding="utf-8") + else: + print(payload) + + summary = report["summary"] + print( + "HOST_SERVICE_CONFIG_INVENTORY_OK " + f"surfaces={summary['surface_count']} " + f"hosts={summary['expected_host_scope_count']} " + f"write_capable={summary['write_capable_surface_count']} " + f"runtime_gate={summary['runtime_gate_count']}", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 12652ee7..18e6dbd9 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -88,6 +88,7 @@ def validate(root: Path) -> None: rollout_policy = load_json(security_dir / "security-rollout-policy.snapshot.json") 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") 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") @@ -2452,6 +2453,142 @@ def validate(root: Path) -> None: [item["category_id"] for item in high_value_config_coverage["lowest_coverage_categories"]], "docker_compose_systemd_host_config", ) + docker_systemd_category = next( + item + for item in high_value_config_coverage["coverage_categories"] + if item["category_id"] == "docker_compose_systemd_host_config" + ) + assert_equal( + "high_value_config_coverage.coverage_categories.docker.coverage_percent", + docker_systemd_category["coverage_percent"], + 50, + ) + assert_equal( + "high_value_config_coverage.coverage_categories.docker.coverage_status", + docker_systemd_category["coverage_status"], + "repo_only_inventory_ready_needs_live_owner_evidence", + ) + for evidence_ref in [ + "docs/security/HOST-SERVICE-CONFIG-INVENTORY.md", + "docs/security/host-service-config-inventory.snapshot.json", + ]: + assert_contains( + "high_value_config_coverage.coverage_categories.docker.evidence_refs", + docker_systemd_category["evidence_refs"], + evidence_ref, + ) + assert_equal( + "host_service_config_inventory.schema", + host_service_config_inventory["schema_version"], + "host_service_config_inventory_v1", + ) + assert_equal( + "host_service_config_inventory.status", + host_service_config_inventory["status"], + "repo_only_inventory_ready", + ) + assert_equal( + "host_service_config_inventory.source_scope", + host_service_config_inventory["source_scope"], + "committed_repo_files_only", + ) + expected_host_service_config_summary = { + "surface_count": 9, + "source_exists_count": 9, + "expected_host_scope_count": 5, + "docker_compose_source_count": 5, + "host_repair_whitelist_count": 2, + "systemd_restart_surface_count": 1, + "write_capable_surface_count": 3, + "surfaces_requiring_owner_response_count": 9, + "surfaces_requiring_live_evidence_count": 8, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "live_evidence_received_count": 0, + "restart_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + "coverage_percent_before_inventory": 42, + "coverage_percent_after_inventory": 50, + } + for key, expected in expected_host_service_config_summary.items(): + assert_equal( + f"host_service_config_inventory.summary.{key}", + host_service_config_inventory["summary"][key], + expected, + ) + for key, value in host_service_config_inventory["execution_boundaries"].items(): + assert_false(f"host_service_config_inventory.execution_boundaries.{key}", value) + assert_equal( + "host_service_config_inventory.config_surfaces.count", + len(host_service_config_inventory["config_surfaces"]), + 9, + ) + host_service_surface_ids = [item["surface_id"] for item in host_service_config_inventory["config_surfaces"]] + for surface_id in [ + "repair_bot_110_whitelist", + "repair_bot_188_whitelist", + "monitoring_110_compose", + "config_backup_host_capture", + ]: + assert_contains( + "host_service_config_inventory.config_surfaces", + host_service_surface_ids, + surface_id, + ) + for surface in host_service_config_inventory["config_surfaces"]: + assert_true( + f"host_service_config_inventory.config_surfaces.{surface['surface_id']}.source_exists", + surface["source_exists"], + ) + assert_equal( + f"host_service_config_inventory.config_surfaces.{surface['surface_id']}.sha256_length", + len(surface["sha256"]), + 64, + ) + assert_true( + f"host_service_config_inventory.config_surfaces.{surface['surface_id']}.requires_owner_response", + surface["requires_owner_response"], + ) + for key in [ + "owner_response_received", + "owner_response_accepted", + "live_evidence_received", + "restart_window_accepted", + "rollback_owner_accepted", + "runtime_gate_open", + "action_buttons_allowed", + ]: + assert_false( + f"host_service_config_inventory.config_surfaces.{surface['surface_id']}.{key}", + surface[key], + ) + assert_equal( + "host_service_config_inventory.write_capable_surfaces.count", + len(host_service_config_inventory["write_capable_surfaces"]), + 3, + ) + for surface_id in [ + "ansible_docker_compose_service_role", + "repair_bot_110_whitelist", + "repair_bot_188_whitelist", + ]: + assert_contains( + "host_service_config_inventory.write_capable_surfaces", + [item["surface_id"] for item in host_service_config_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", + ]: + assert_contains( + "iwooos_projection.source_paths.host_service_config_inventory", + iwooos_projection["source_paths"], + source_path, + ) assert_true( "iwooos_projection.summary.high_value_config_control_coverage_first_layer", iwooos_projection["summary"]["high_value_config_control_coverage_first_layer"], @@ -2485,6 +2622,33 @@ def validate(root: Path) -> None: iwooos_projection["summary"][key], expected, ) + expected_host_service_projection_summary = { + "host_service_config_inventory_first_layer": True, + "host_service_config_inventory_surface_count": 9, + "host_service_config_inventory_source_exists_count": 9, + "host_service_config_inventory_expected_host_scope_count": 5, + "host_service_config_inventory_docker_compose_source_count": 5, + "host_service_config_inventory_host_repair_whitelist_count": 2, + "host_service_config_inventory_systemd_restart_surface_count": 1, + "host_service_config_inventory_write_capable_surface_count": 3, + "host_service_config_inventory_owner_response_required_count": 9, + "host_service_config_inventory_owner_response_received_count": 0, + "host_service_config_inventory_owner_response_accepted_count": 0, + "host_service_config_inventory_live_evidence_required_count": 8, + "host_service_config_inventory_live_evidence_received_count": 0, + "host_service_config_inventory_restart_window_accepted_count": 0, + "host_service_config_inventory_rollback_owner_accepted_count": 0, + "host_service_config_inventory_runtime_gate_count": 0, + "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, + } + for key, expected in expected_host_service_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"], @@ -11594,6 +11758,11 @@ def validate(root: Path) -> None: iwooos_projection_page, "IwoooSHighValueConfigControlCoverageBoard", ) + assert_text_contains( + "iwooos_page.high_value_config_control_coverage_docker_systemd_percent", + iwooos_projection_page, + "{ key: 'dockerSystemd', rank: 'P1-1', value: '50%'", + ) assert_text_before( "iwooos_page.high_value_config_control_coverage_before_owner_packet", iwooos_projection_page, @@ -11613,6 +11782,13 @@ def validate(root: Path) -> None: "high_value_config_control_coverage_owner_response_accepted_count=0", "high_value_config_control_coverage_runtime_gate_count=0", "high_value_config_control_coverage_action_button_count=0", + "host_service_config_inventory_surface_count=9", + "host_service_config_inventory_write_capable_surface_count=3", + "host_service_config_inventory_runtime_gate_count=0", + "docker_compose_action_authorized=false", + "systemctl_action_authorized=false", + "repair_bot_execution_authorized=false", + "ansible_apply_authorized=false", "runtime_execution_authorized=false", "host_write_authorized=false", "nginx_reload_authorized=false",