Files
awoooi/scripts/security/domain-tls-certbot-owner-response-acceptance.py
Your Name 066bf5d1be
All checks were successful
Code Review / ai-code-review (push) Successful in 15s
fix(iwooos): 新增 dns tls owner acceptance ledger
2026-06-14 22:46:40 +08:00

391 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
IwoooS DNS / TLS / certbot owner response acceptance 只讀帳本產生器。
本工具讀取 Domain / TLS / certbot owner confirmation request 草稿,建立未來
owner response 如何收件、補證、隔離、拒收或進 reviewer review 的
metadata-only acceptance ledger。它不做 DNS query、不做 live TLS probe、
不執行 certbot renew、不 reload Nginx、不 SSH、不讀 private key。
"""
from __future__ import annotations
import argparse
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))
ACCEPTANCE_FIELDS = [
"acceptance_candidate_id",
"request_id",
"domain",
"control_tier",
"hosts",
"certificate_path_domains",
"certificate_paths",
"owner_response_ref",
"owner_role_or_team",
"decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"certificate_coverage_basis_ref",
"certificate_expiry_metadata_ref",
"renewal_owner",
"acme_challenge_route_owner",
"maintenance_window",
"rollback_owner",
"validation_plan",
"reviewer_outcome",
"followup_owner",
"not_approval",
]
REQUIRED_OWNER_RESPONSE_FIELDS = [
"owner_role_or_team",
"decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"certificate_coverage_basis_ref",
"certificate_expiry_metadata_ref",
"renewal_owner",
"acme_challenge_route_owner",
"maintenance_window",
"rollback_owner",
"validation_plan",
"followup_owner",
]
REVIEWER_CHECKS = [
{
"check_id": "owner_identity_present",
"instruction": "owner role / team 必須可追溯,不能只寫個人暱稱或聊天同意。",
},
{
"check_id": "decision_and_reason_present",
"instruction": "decision 與 decision reason 必須同時存在,且不得包含機敏值。",
},
{
"check_id": "redacted_refs_only",
"instruction": "evidence 只能是脫敏 ref、ticket、hash、文件路徑或摘要。",
},
{
"check_id": "raw_certificate_absent",
"instruction": "不得出現 raw certificate payload、完整 SAN dump、private key 或 ACME account key。",
},
{
"check_id": "secret_value_absent",
"instruction": "不得出現 DNS credential、registrar credential、token、cookie、authorization header 或 Basic Auth。",
},
{
"check_id": "coverage_basis_ref_present",
"instruction": "SAN、wildcard 或共用憑證覆蓋依據必須是 metadata ref不得貼 raw cert。",
},
{
"check_id": "expiry_metadata_ref_not_probe",
"instruction": "憑證到期資訊只能是 owner-provided metadata ref不得由本帳本觸發 live probe。",
},
{
"check_id": "renewal_owner_separate_from_action",
"instruction": "renewal owner 可以被記錄,但 certbot renew 需另開人工批准包。",
},
{
"check_id": "acme_route_owner_present",
"instruction": "HTTP-01 ACME path 與 route smoke owner 必須清楚,且不得自動 route smoke。",
},
{
"check_id": "maintenance_window_present",
"instruction": "任何未來 probe、renew、reload 都必須有維護窗口或明確禁止窗口。",
},
{
"check_id": "rollback_owner_present",
"instruction": "rollback owner 與 rollback ref 必須存在,且不可指向 raw secret。",
},
{
"check_id": "validation_plan_present",
"instruction": "validation plan 必須列 DNS / TLS / ACME / route post-check 指標,但不授權執行。",
},
{
"check_id": "counts_transition_safe",
"instruction": "只有 reviewer record 可更新 received / accepted / rejected不得同時開 runtime gate。",
},
]
OUTCOME_LANES = [
{
"lane_id": "waiting_owner_response",
"meaning": "尚未收到 owner response所有 accepted / runtime count 維持 0。",
},
{
"lane_id": "quarantine_raw_certificate_or_secret",
"meaning": "收到 raw cert、private key、DNS credential 或 certbot account 內容時,只能隔離。",
},
{
"lane_id": "reject_execution_request",
"meaning": "夾帶 DNS query、TLS probe、certbot renew、Nginx reload 或 host write 要求時拒收。",
},
{
"lane_id": "request_supplement",
"meaning": "欄位不足、scope 不清或 evidence ref 不可追溯時要求補件。",
},
{
"lane_id": "ready_for_certificate_coverage_review",
"meaning": "metadata 合格後,只能進憑證覆蓋關係 reviewer review不自動 probe。",
},
{
"lane_id": "owner_review_only_update",
"meaning": "只允許更新只讀 owner review ledger不得修改 DNS、TLS、certbot 或 Nginx。",
},
{
"lane_id": "waiting_runtime_gate",
"meaning": "即使 owner response acceptedDNS / TLS / certbot runtime gate 仍等待獨立人工批准。",
},
]
BLOCKED_ACTIONS = [
"perform_dns_query",
"perform_live_tls_probe",
"run_certbot_renew",
"run_nginx_reload",
"run_route_smoke",
"read_tls_private_key",
"store_raw_certificate_payload",
"store_dns_provider_credential",
"store_certbot_account_key",
"collect_secret_value",
"accept_unredacted_certbot_log",
"accept_shell_history_or_env_dump",
"mark_owner_response_accepted_without_reviewer_record",
"mark_certificate_coverage_confirmed_without_ref",
"modify_dns_record",
"modify_tls_certificate_path",
"modify_acme_challenge_route",
"write_production_host",
"open_runtime_gate",
"add_action_button",
]
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 load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def acceptance_candidate(request: dict[str, Any]) -> dict[str, Any]:
request_id = request["request_id"]
return {
"acceptance_candidate_id": request_id.replace(
"domain_tls_certbot_owner_confirmation:",
"domain_tls_certbot_owner_response_acceptance:",
),
"status": "waiting_owner_response",
"request_id": request_id,
"domain": request["domain"],
"control_tier": request["control_tier"],
"hosts": request["hosts"],
"certificate_path_domains": request["certificate_path_domains"],
"certificate_paths": request["certificate_paths"],
"owner_response_ref": None,
"owner_role_or_team": "pending_owner_response",
"decision": "pending_owner_response",
"decision_reason": "pending_owner_response",
"affected_scope": "pending_owner_response",
"redacted_evidence_refs": [],
"certificate_coverage_basis_ref": None,
"certificate_expiry_metadata_ref": None,
"renewal_owner": "pending_owner_response",
"acme_challenge_route_owner": "pending_owner_response",
"maintenance_window": "pending_owner_response",
"rollback_owner": "pending_owner_response",
"validation_plan": "pending_owner_response",
"reviewer_outcome": "waiting_owner_response",
"followup_owner": "pending_owner_response",
"acceptance_fields": ACCEPTANCE_FIELDS,
"required_owner_response_fields": REQUIRED_OWNER_RESPONSE_FIELDS,
"reviewer_checks": [item["check_id"] for item in REVIEWER_CHECKS],
"outcome_lanes": [item["lane_id"] for item in OUTCOME_LANES],
"blocked_actions": BLOCKED_ACTIONS,
"not_approval": True,
"request_sent": False,
"recipient_confirmed": False,
"owner_response_received": False,
"owner_response_accepted": False,
"owner_response_rejected": False,
"owner_response_quarantined": False,
"supplement_requested": False,
"certificate_coverage_confirmed": False,
"certificate_expiry_metadata_accepted": False,
"renewal_owner_accepted": False,
"acme_route_owner_accepted": False,
"maintenance_window_accepted": False,
"rollback_owner_accepted": False,
"dns_query_authorized": False,
"dns_query_executed": False,
"live_tls_probe_authorized": False,
"live_tls_probe_executed": False,
"certbot_renew_authorized": False,
"certbot_renew_executed": False,
"nginx_reload_authorized": False,
"nginx_reload_executed": False,
"route_smoke_authorized": False,
"route_smoke_executed": False,
"host_write_authorized": False,
"production_write_authorized": False,
"runtime_gate": False,
"action_buttons_allowed": False,
"secret_value_collection_allowed": False,
}
def build_report(
root: Path,
request_report: dict[str, Any],
generated_at: str | None,
) -> dict[str, Any]:
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
requests = request_report.get("owner_confirmation_requests", [])
acceptance_candidates = [acceptance_candidate(item) for item in requests]
c0_candidates = [item for item in acceptance_candidates if item["control_tier"] == "C0"]
return {
"schema_version": "domain_tls_certbot_owner_response_acceptance_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"source_owner_confirmation_request_schema_version": request_report.get("schema_version"),
"source_owner_confirmation_request_status": request_report.get("status"),
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
"summary": {
"source_owner_confirmation_request_count": request_report.get("summary", {}).get(
"owner_confirmation_request_count", 0
),
"acceptance_candidate_count": len(acceptance_candidates),
"c0_acceptance_candidate_count": len(c0_candidates),
"acceptance_field_count": len(ACCEPTANCE_FIELDS),
"required_owner_response_field_count": len(REQUIRED_OWNER_RESPONSE_FIELDS),
"reviewer_check_count": len(REVIEWER_CHECKS),
"outcome_lane_count": len(OUTCOME_LANES),
"blocked_action_count": len(BLOCKED_ACTIONS),
"request_sent_count": 0,
"recipient_confirmed_count": 0,
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"owner_response_rejected_count": 0,
"owner_response_quarantined_count": 0,
"supplement_requested_count": 0,
"certificate_coverage_confirmed_count": 0,
"certificate_expiry_metadata_accepted_count": 0,
"renewal_owner_accepted_count": 0,
"acme_route_owner_accepted_count": 0,
"maintenance_window_accepted_count": 0,
"rollback_owner_accepted_count": 0,
"dns_query_authorized_count": 0,
"dns_query_executed_count": 0,
"live_tls_probe_authorized_count": 0,
"live_tls_probe_executed_count": 0,
"certbot_renew_authorized_count": 0,
"certbot_renew_executed_count": 0,
"nginx_reload_authorized_count": 0,
"nginx_reload_executed_count": 0,
"route_smoke_authorized_count": 0,
"route_smoke_executed_count": 0,
"host_write_authorized_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
"execution_boundaries": {
"request_dispatch_authorized": False,
"owner_response_accepted": False,
"raw_certificate_storage_allowed": False,
"tls_private_key_read_authorized": False,
"secret_value_collection_allowed": False,
"dns_query_authorized": False,
"dns_query_executed": False,
"live_tls_probe_authorized": False,
"live_tls_probe_executed": False,
"certbot_renew_authorized": False,
"certbot_renew_executed": False,
"nginx_reload_authorized": False,
"nginx_reload_executed": False,
"route_smoke_authorized": False,
"route_smoke_executed": False,
"host_write_authorized": False,
"production_write_authorized": False,
"runtime_execution_authorized": False,
"action_buttons_allowed": False,
"not_authorization": True,
},
"acceptance_fields": ACCEPTANCE_FIELDS,
"required_owner_response_fields": REQUIRED_OWNER_RESPONSE_FIELDS,
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"blocked_actions": BLOCKED_ACTIONS,
"acceptance_candidates": acceptance_candidates,
"next_steps": [
"等待 owner response未收到前不得更新 accepted count。",
"收到回覆後先走 raw cert / private key / credential / execution request 檢查,不合格即隔離、拒收或補件。",
"metadata 合格也只能進 certificate coverage reviewer reviewDNS query、TLS probe、certbot renew、Nginx reload 與 route smoke 仍需獨立人工批准。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS DNS / TLS / certbot owner response acceptance 只讀帳本產生器")
parser.add_argument("--root", default=".", help="repo root")
parser.add_argument(
"--owner-confirmation-request-report",
default="docs/security/domain-tls-certbot-owner-confirmation-request.snapshot.json",
help="domain-tls-certbot-owner-confirmation-request.py 輸出的 JSON",
)
parser.add_argument("--output", help="寫出 JSON 報告")
parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用")
args = parser.parse_args()
root = Path(args.root).resolve()
request_report = load_json(root / args.owner_confirmation_request_report)
report = build_report(root, request_report, args.generated_at)
payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
if args.output:
output = root / 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(
"DOMAIN_TLS_CERTBOT_OWNER_RESPONSE_ACCEPTANCE_OK "
f"candidates={summary['acceptance_candidate_count']} "
f"c0={summary['c0_acceptance_candidate_count']} "
f"checks={summary['reviewer_check_count']} "
f"lanes={summary['outcome_lane_count']} "
f"accepted={summary['owner_response_accepted_count']} "
f"runtime_gate={summary['runtime_gate_count']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())