# 边缘网关安装、证书与发布运行手册 适用对象:平台运维、企业网络、安全、数据库和验收人员。本手册描述可在本地验证的工程基线;当前最高状态为 `ENGINEERING_COMPLETE_ENTERPRISE_EDGE_UAT_BLOCKED`。`enterprise_source_1`、 `enterprise_source_2`、`network`、`security_legal` 均为 `TBD_EXTERNAL`,不得把本手册的本地验证解释为真实企业验收。 ## 1. 安全边界与职责 - 边缘主动以 HTTPS/mTLS 拉取,不开放入站控制端口。企业网络负责人批准精确控制面/代理 origin、DNS、出站防火墙、CA 与 CRL。 - 平台控制面签发一次性注册令牌、任务和发布 Ed25519 签名;私钥由对应安全域保管。 - 边缘私钥只在本地生成,权限 `0600`;根目录、queue、artifacts、releases、secrets 目录权限 `0700`。 - 默认拒绝:任意命令、任意 SQL、任意 URL、路径穿越、符号链接、通配 host、HTTP、原始行、近期明细、凭据、私钥和本地路径均不可出域。 - 数据分级:`raw`、`recent_detail`、`restricted` 仅边缘保存;控制面只接受 `desensitized_metadata`、`statistics`、`lineage`、`evidence`、`health_summary`、`diagnostic_summary`。原始数据保留周期由企业制度批准;近期明细工程上限 365 天;控制面事件按已批准最小保留期执行。 - PostgreSQL 迁移 head 为 `20260809_479`。应用角色不能创建/轮换/撤销 signing key;仅 migration/security-owner 路径可写受控 signing state。 ## 2. 前置条件 零主机依赖基线(低于该版本必须先由平台运维完成兼容性评估):Docker Engine 26、 Docker Compose 2.27、Python 3.11、OpenSSL 3、SQLite 3.40、curl 8 和 cosign 2.2。 主机不安装数据库驱动、企业来源 SDK 或编译器;这些只能进入经签名的 Edge 镜像。 1. 在批准的 Linux 主机建立专用非 root 账号,记录其数值 UID/GID,确认磁盘加密、时间同步、审计和备份位置。`init` 参数、`edge.env` 和 Compose `user` 必须使用同一组 UID/GID;不一致时工具拒绝继续,只有 root 可显式使用 `--allow-chown` 安全调整既有空目录所有权。 2. 网络负责人书面给出精确控制 origin、可选代理 origin、DNS/IP、证书链、CRL 更新频率和出站策略。 3. 安全负责人交付任务/发布 Ed25519 公钥集;不得交付控制面签名私钥。 4. 数据负责人批准可执行来源、用途、环境、网络区、分类、保留期和只读账号。 5. 确认控制面数据库已由 migration owner 升到 479;应用数据库角色经过最小权限验证。 以下示例固定使用绝对根目录 `/opt/dataops-edge`;如企业批准其他目录,逐条替换并重新留证,不得使用相对路径。 发布包必须来自受控制品库。下载到空的 `/opt/dataops-release`,先核对制品库登记的 tar SHA-256 和离线签名,再展开并验证包内每一个文件。下面的公钥、签名和摘要均是 企业批准后注入的占位路径,不能用仓库示例代替: ```bash sudo install -d -m 0700 /opt/dataops-release cd /opt/dataops-release sha256sum dataops-platform-release-.tar.gz cosign verify-blob --key /etc/dataops/trust/release-cosign.pub \ --signature dataops-platform-release-.tar.gz.sig \ dataops-platform-release-.tar.gz sudo tar -xzf dataops-platform-release-.tar.gz -C /opt cd /opt/dataops-platform sha256sum -c SHA256SUMS ``` 任一步失败:隔离下载文件、记录制品 digest 和失败码、从受控制品库重新取件;禁止 跳过验签、手工改包或继续启动。以上只证明交付完整性,不证明企业来源/网络 UAT。 ## 3. 初始化与 CSR ```bash sudo python3 /opt/dataops-platform/scripts/edge_gateway_admin.py init \ --root /opt/dataops-edge \ --runtime-uid 65532 \ --runtime-gid 65532 sudo python3 /opt/dataops-platform/scripts/edge_gateway_admin.py csr --root /opt/dataops-edge --common-name edge-prod-zone-a ``` 验收:JSON 输出仅含路径、指纹和状态;`/opt/dataops-edge/secrets/client.key` 为 `0600`,CSR 签名有效,stdout/stderr/审计没有私钥内容。失败时停止注册,隔离并销毁不完整材料后重新初始化;不得降低权限或上传 key。 由企业 CA 对 `/opt/dataops-edge/certificates/client.csr` 签发客户端证书。将证书、CA bundle、CRL 写入批准证书目录并设为只读挂载源。验证证书用途、有效期、链、CRL 和 CSR/key 公钥一致: ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py register-manifest \ --root /opt/dataops-edge \ --certificate /opt/dataops-edge/certificates/client.pem \ --ca-bundle /opt/dataops-edge/certificates/ca.pem \ --crl /opt/dataops-edge/certificates/control-plane.crl.pem \ --gateway-id 11111111-1111-4111-8111-111111111111 \ --environment production \ --network-zone zone-a \ --policy-digest aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ --control-origin https://control.example.invalid ``` 工具只写固定目标 `/opt/dataops-edge/registration/registration.json`,不会覆盖 secrets、certificates、CSR、queue 或 releases。它校验证书有效期、clientAuth EKU、key/CSR/certificate 公钥一致、CA 签名、CRL 签名/有效期/撤销状态;相同内容重放幂等,已有不同内容则冲突。注册材料只含 digest、公钥绑定和 CA/CRL 证据字段,不含本地绝对路径、私钥或 credential。通过受批准的人工/平台通道提交它和一次性注册令牌;令牌只能使用一次且失败绑定不得消耗。控制面返回的 edge credential 立即写入 `/opt/dataops-edge/secrets/control-credential`(`0600`),不得进入 shell history、环境文件或工单正文。 ### 3.1 控制面注册、轮换、撤销与机器 ACK 下列命令均使用占位符,执行前由平台运维把 origin、ID 和 digest 替换为批准值。 Bearer、enrollment token 和 edge credential 只放在 `0600` header 文件;不得把值 直接写进命令行。`human-auth.headers` 由企业统一认证流程生成,本手册不生成弱口令。 先以 `umask 077` 创建以下闭合请求文件;字段外内容一律拒绝。示例 UUID、digest、 deadline 和 lease 只是占位,必须替换为控制面返回或批准台账中的精确值: ```json // /run/operator/enrollment-request.json {"gateway_name":"edge-prod-zone-a","environment":"production","network_zone":"zone-a","policy_digest":"<64-hex>","allowed_control_hosts":["control.example.invalid"],"allowed_proxy_hosts":[],"expected_certificate_sha256":"<64-hex>","ttl_seconds":3600} // /run/operator/rotate-request.json {"certificate_sha256":"","request_id":"rotate-2026-08-zone-a"} // /run/edge/outcome-request.json {"environment":"production","network_zone":"zone-a","generation":1,"outcome":"completed","lease_token":"","safe_summary":{"status":"completed"}} // /run/edge/reconcile-request.json {"environment":"production","network_zone":"zone-a","generation":1,"limit":50,"cancel_cursor":null,"release_cursor":null} // /run/edge/release-installed-ack.json {"environment":"production","network_zone":"zone-a","generation":1,"release_id":"","outcome":"installed","safe_summary":{"release_status":"installed","version":"2.0.0"}} ``` 注释行仅用于说明,真正保存时每个文件只保留对应的一行 JSON;运行前用 OpenAPI `EdgeEnrollmentRequest`、`EdgeRotateRequest`、`EdgeTaskOutcomeRequest`、 `EdgeReconcileRequest`、`EdgeReleaseAckRequest` 校验。`lease_token` 只由 Agent 持久化加密 并自动发送,人工 curl 仅限事故演练,演练文件用后立即销毁。 平台人员创建 enrollment(HTTP 201;验收字段为 enrollment_id、gateway_id、 expires_in、returned_once): ```bash umask 077 curl --fail-with-body --silent --show-error \ --header @/run/operator/human-auth.headers --header 'Content-Type: application/json' \ --output /run/operator/enrollment-response.json \ --data @/run/operator/enrollment-request.json \ https://control.example.invalid/api/datasource/edge/enrollments ``` `enrollment-request.json` 必须含 gateway_name、environment、network_zone、policy_digest、 allowed_control_hosts、allowed_proxy_hosts、expected_certificate_sha256。把一次性 token 从响应安全写入 header 文件,既不打印也不进入 shell history: ```bash python3 - /run/operator/enrollment-response.json /run/operator/enrollment.headers <<'PY' import json, os, sys value = json.load(open(sys.argv[1], encoding="utf-8"))["data"]["enrollment_token"] fd = os.open(sys.argv[2], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as target: target.write("X-Edge-Enrollment: " + value + "\n") PY ``` 边缘以 mTLS 注册(HTTP 201;验收 generation=1、certificate_sha256 精确匹配、 credential_returned_once=true): ```bash curl --fail-with-body --silent --show-error \ --cert /opt/dataops-edge/certificates/client.pem \ --key /opt/dataops-edge/secrets/client.key \ --cacert /opt/dataops-edge/certificates/ca.pem \ --header @/run/operator/enrollment.headers --header 'Content-Type: application/json' \ --output /run/operator/registration-response.json \ --data @/opt/dataops-edge/registration/registration.json \ https://control.example.invalid/api/datasource/edge/register ``` 安全落盘 credential,同时生成 curl 使用的 `X-Edge-Credential` header;两个文件均 `0600`,脚本不打印值: ```bash python3 - /run/operator/registration-response.json \ /opt/dataops-edge/secrets/control-credential /opt/dataops-edge/secrets/edge-auth.headers <<'PY' import json, os, sys value = json.load(open(sys.argv[1], encoding="utf-8"))["data"]["credential"] for path, prefix in ((sys.argv[2], ""), (sys.argv[3], "X-Edge-Credential: ")): fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as target: target.write(prefix + value + "\n") PY shred -u /run/operator/registration-response.json /run/operator/enrollment-response.json ``` 平台人员轮换/撤销(均 HTTP 200;轮换验收 generation 增加且旧 credential 失效; 撤销验收 revoked=true、旧身份随后返回 401/403): ```bash curl --fail-with-body --header @/run/operator/human-auth.headers \ --header 'Content-Type: application/json' --data @/run/operator/rotate-request.json \ https://control.example.invalid/api/datasource/edge/gateways//rotate curl --fail-with-body --request POST --header @/run/operator/human-auth.headers \ https://control.example.invalid/api/datasource/edge/gateways//revoke ``` 机器 outcome、reconcile 和 installed ACK 必须同时使用 mTLS 与 credential header。 `/tasks/{task_id}/outcome` 的 `{task_id}` 是文档占位符,实际替换为已租赁任务 ID: ```bash curl --fail-with-body --cert /opt/dataops-edge/certificates/client.pem \ --key /opt/dataops-edge/secrets/client.key --cacert /opt/dataops-edge/certificates/ca.pem \ --header @/opt/dataops-edge/secrets/edge-auth.headers --header 'Content-Type: application/json' \ --data @/run/edge/outcome-request.json \ https://control.example.invalid/api/datasource/edge/gateways//tasks//outcome curl --fail-with-body --cert /opt/dataops-edge/certificates/client.pem \ --key /opt/dataops-edge/secrets/client.key --cacert /opt/dataops-edge/certificates/ca.pem \ --header @/opt/dataops-edge/secrets/edge-auth.headers --header 'Content-Type: application/json' \ --data @/run/edge/reconcile-request.json \ https://control.example.invalid/api/datasource/edge/gateways//reconcile curl --fail-with-body --cert /opt/dataops-edge/certificates/client.pem \ --key /opt/dataops-edge/secrets/client.key --cacert /opt/dataops-edge/certificates/ca.pem \ --header @/opt/dataops-edge/secrets/edge-auth.headers --header 'Content-Type: application/json' \ --data @/run/edge/release-installed-ack.json \ https://control.example.invalid/api/datasource/edge/gateways//releases/ack ``` outcome 验收为 completed/failed/cancelled 的幂等确认;reconcile 验收 cancel/release cursor 单调且数组不超过 50;installed ACK 验收 release ID、artifact digest 和当前 verified pointer 一致。4xx 先检查绑定/重放,不重建 credential;5xx/断网保留本地 outbox 并按阈值退避,不手工伪造 ACK。 ## 4. 部署与启动验收 1. 复制 `/opt/dataops-platform/deploy/edge/edge.env.example` 到 `/opt/dataops-edge/edge.env`,设为 `0600`,将 `EDGE_RUNTIME_UID/GID` 填成初始化时批准的同一数值,只填写目录、不可变镜像 digest 和精确 origin;credential 不写入 env。仓库默认的 `dataops-edge:local` 只允许本地工程验证,生产必须把 `EDGE_IMAGE` 和 `EDGE_PULL_POLICY=never` 固定到已验签 digest。 2. 本地工程镜像可在隔离构建区从归档根目录构建;该构建可能访问批准的 base image/ Python 包镜像,因此不能在企业 Edge 运行主机临时构建。企业 Edge 主机只从已验签 镜像仓库按 digest 拉取。两种路径都必须记录最终 digest,禁止只用可变 tag: ```bash docker build --pull=false -f deploy/edge/Dockerfile.edge -t dataops-edge: . docker image inspect --format '{{.Id}}' dataops-edge: # 企业制品库路径:先由安全方验证签名,再拉 immutable digest cosign verify --key /etc/dataops/trust/image-cosign.pub \ registry.example.invalid/dataops-edge@sha256: docker pull registry.example.invalid/dataops-edge@sha256: ``` 3. 将 `deploy/edge/edge.config.example.json` 复制为 `/opt/dataops-edge/config/edge.json`。 `deploy/edge/edge.config.schema.json` 是该文件的完整闭合 schema:除 schema 所列字段外均拒绝。 credential、task/release 公钥和 server CRL 路径不在 `edge.json`;它们分别从 `secrets/control-credential`、`signing/*.json` 和 Compose 固定只读挂载注入。校验: ```bash python3 -c 'import json,jsonschema; s=json.load(open("deploy/edge/edge.config.schema.json")); d=json.load(open("/opt/dataops-edge/config/edge.json")); jsonschema.Draft202012Validator(s,format_checker=jsonschema.FormatChecker()).validate(d)' sudo install -d -m 0700 -o 65532 -g 65532 /opt/dataops-edge/{config,queue,artifacts,releases,secrets,certificates,signing} chmod 0600 /opt/dataops-edge/config/edge.json /opt/dataops-edge/secrets/client.key find /opt/dataops-edge -xdev -type d -exec stat -f '%Su:%Sg %Lp %N' {} \; find /opt/dataops-edge -xdev -type f -exec stat -f '%Su:%Sg %Lp %N' {} \; ``` Linux 上将最后两条 `stat -f` 替换为 `stat -c '%U:%G %a %n'`。验收要求运行 UID/GID 可读 config/cert/key/signing,只能写 queue/artifacts/releases;其他账号不可读 key/credential。 3. 渲染而不启动: ```bash docker compose --env-file /opt/dataops-edge/edge.env -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml config ``` 4. 安全和网络批准渲染结果后再启动: ```bash docker compose --env-file /opt/dataops-edge/edge.env -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml up -d ``` 启动后最多等待 120 秒。首次 health 必须是 `healthy`;`degraded` 只允许在批准的 断网演练中出现,`stopped` 表示身份/CRL fail-stop,不能强行复活: ```bash healthy=0 for n in $(seq 1 24); do docker compose --env-file /opt/dataops-edge/edge.env \ -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml ps docker compose --env-file /opt/dataops-edge/edge.env \ -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml exec -T edge-gateway \ python -m app.edge_gateway.healthcheck && { healthy=1; break; } sleep 5 done if [ "$healthy" -ne 1 ]; then docker compose --env-file /opt/dataops-edge/edge.env \ -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml stop edge-gateway exit 1 fi docker compose --env-file /opt/dataops-edge/edge.env \ -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml exec -T edge-gateway \ python -c 'import json; print(json.load(open("/run/edge/tmp/health.json"))["status"])' ``` 验收:无 `ports`/`expose`;进程 UID 非 0;rootfs 只读;capabilities 全部移除;证书、secret、signing 公钥只读;仅 queue/artifact/release 可写;`health.json` 输出只含有界状态,不含 credential、key、原始行、SQL 或路径。120 秒未健康则停止容器、保留 health/计数/digest,检查 CRL、证书、exact origin 和文件 owner;禁止扩大 host allowlist。 ## 5. 断网、重连、取消与诊断 - 断网:边缘先持久化已签名任务和出站事件,按有界指数退避重试;不得绕过 allowlist 改用其他地址。达到安全上限进入人工处置。 - 重连:先 reconcile 取消游标、发布游标和 release baseline,再拉取新任务;稳定 event ID 的同 digest 重放返回原 ACK,异 digest 冲突并告警。 - 取消:控制面置 cancel-requested;边缘在执行前、执行中合作点和出站前检查。不能安全中断的本地执行应停止派发新工作并形成安全摘要。 - 诊断:只允许计数、版本、队列水位、最后心跳、分类化错误码和 digest。禁止日志正文包含源数据、近期明细、credential、lease、证书、私钥、SQL、连接串或本地绝对数据路径。 - CRL 命中或证书撤销:mTLS 代理拒绝连接;边缘写本地停机标记并停止控制流。不得通过本地命令解除撤销。 只读诊断统一使用下列命令;输出只有 queue/release/lease/cursor/outcome/artifact cleanup 计数和 digest,不含本地路径或秘密: ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge ``` 人工介入阈值由企业可观测负责人确认,未批准更宽阈值前使用以下上限: | 信号 | 告警 | 人工介入阈值 | 安全动作/恢复验收 | |---|---:|---:|---| | 控制面连续失败 | 3 次或 60 秒 | 10 次或 15 分钟 | 保持 pull-only,禁止换 host;恢复后先 reconcile | | queue 待发事件 | 1,000 | 10,000 | 冻结新任务;确认 ACK 后再降水位 | | queue 运行/lease | 最老 5 分钟 | 最老 15 分钟 | 停止新 lease,验证 fencing,不手工删行 | | queue/artifact 分区 | 剩余 20% | 剩余 10% | 冻结采集;先 legal hold/retention 决策再清理 | | server CRL | 距 nextUpdate 24 小时 | 距 nextUpdate 1 小时或已过期 | fail-stop;更新并验签后重启 | | release rollback intent | 任意存在 | 5 分钟未清理 | 禁止新发布;按 intent/history 恢复 | 每次告警记录 gateway digest、时间、计数、版本和批准工单号;不得附带源记录、SQL、 credential 或证书私钥。阈值恢复验收要求连续 3 个心跳 healthy、queue 水位下降、 reconcile cursor 单调和无新签名/身份错误。 ### 5.1 冻结、取消、强制 reconcile 与 drain 当前工程基线没有可绕过签名/租约的“强制执行”入口。冻结新 pull 的唯一受控方式是 停止 Agent;控制面可继续登记 cancel-requested。恢复时启动 Agent,启动循环会先完整 reconcile,再拉取新任务: ```bash COMPOSE=/opt/dataops-platform/deploy/edge/docker-compose.edge.yml ENV=/opt/dataops-edge/edge.env docker compose --env-file "$ENV" -f "$COMPOSE" stop edge-gateway curl --fail-with-body --request POST --header @/run/operator/human-auth.headers \ https://control.example.invalid/api/datasource/edge/tasks//cancel sqlite3 -readonly /opt/dataops-edge/queue/edge-queue-v2.sqlite3 \ "SELECT status,count(*) FROM edge_tasks GROUP BY status; SELECT status,count(*) FROM edge_outbound_events GROUP BY status;" docker compose --env-file "$ENV" -f "$COMPOSE" up -d edge-gateway ``` drain 完成条件:task 不含 `pending/leased`,event/outcome 不含 `pending/sending`, artifact cleanup 不含 `deleting`,`status` 显示 active lease=0。若不能 drain,保持容器 停止,保留 queue 副本和 digest;不得直接改状态或删行。恢复后必须观察 reconcile cursor 前进、cancelled outcome ACK 和待发事件下降,再解除变更冻结。 ## 6. 证书轮换与撤销 轮换先生成 pending key/CSR,不覆盖当前有效材料。相同 request-id 和相同 digest 幂等返回;相同 request-id 的不同输入失败。 ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py rotate \ --root /opt/dataops-edge \ --common-name edge-prod-zone-a \ --request-id rotate-2026-08-zone-a \ --expected-current-certificate-digest bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ``` CA 签发 pending CSR 后,先在控制面调用 rotate 并取得新 generation/credential 的服务端批准证据,再原子切换证书/credential;旧材料只保留最多 3 份加密备份并在观察期后销毁。失败时继续使用尚未过期且未撤销的当前 generation;不得半切换。 切换前先把新证书、新私钥和新 credential 分别写入同一文件系统的 `.next` 文件并设 `0600`,验证证书/私钥、公钥、generation 和服务端响应完全匹配。然后停止容器,逐个 原子 rename;容器停止期间任何一步失败都从 `.previous` 恢复,不能以半套身份启动: ```bash COMPOSE=/opt/dataops-platform/deploy/edge/docker-compose.edge.yml ENV=/opt/dataops-edge/edge.env docker compose --env-file "$ENV" -f "$COMPOSE" stop edge-gateway install -m 0600 /run/operator/client.pem.next /opt/dataops-edge/certificates/client.pem.next install -m 0600 /run/operator/client.key.next /opt/dataops-edge/secrets/client.key.next install -m 0600 /run/operator/control-credential.next /opt/dataops-edge/secrets/control-credential.next mv /opt/dataops-edge/certificates/client.pem /opt/dataops-edge/certificates/client.pem.previous mv /opt/dataops-edge/secrets/client.key /opt/dataops-edge/secrets/client.key.previous mv /opt/dataops-edge/secrets/control-credential /opt/dataops-edge/secrets/control-credential.previous mv /opt/dataops-edge/certificates/client.pem.next /opt/dataops-edge/certificates/client.pem mv /opt/dataops-edge/secrets/client.key.next /opt/dataops-edge/secrets/client.key mv /opt/dataops-edge/secrets/control-credential.next /opt/dataops-edge/secrets/control-credential docker compose --env-file "$ENV" -f "$COMPOSE" up -d edge-gateway ``` 120 秒内未 healthy 或新 generation 绑定失败:停止容器、把三个 `.previous` 原子改回 固定文件名并恢复旧 generation 配置;只有旧证书尚未过期且未撤销时才允许回退。成功 连续 3 个心跳后再按企业加密介质制度销毁 response、`.previous` 和临时 header;普通 文件系统上的 `shred` 不能替代加密磁盘/密钥销毁证据。 撤销状态查询: ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py revoke-status --root /opt/dataops-edge python3 /opt/dataops-platform/scripts/edge_gateway_admin.py revoke-status \ --root /opt/dataops-edge \ --server-status /opt/dataops-edge/state/server-revocation.json \ --trusted-key status-key-2026=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \ --expected-gateway-id 11111111-1111-4111-8111-111111111111 \ --expected-generation 1 \ --expected-certificate-digest bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ``` 服务端文件必须是 canonical Ed25519 签名 envelope,并精确绑定 gateway、generation、证书 SHA-256、status、issued/expires 和 key-id;伪签名、过期、跨网关或绑定不符均拒绝。若可信证据为 revoked,工具写 `REVOKED_STOP`;本地 revoked 是终态,即使后续收到 active 也不会自行解除。停止容器、隔离本机、保留安全日志并通知控制面。恢复必须新建 enrollment、证书和 credential;禁止清除标记继续复用旧身份。 ## 7. 签名升级与回滚 控制面签名任务创建只接受完整 `EdgeTaskContract`,签名由服务端产生,操作者不提交 signature。当前镜像内置的唯一可执行任务是工程自检 `quality/quality-evaluation/evidence`; 企业来源 collect/profile/lineage/controlled_query handler 仍为 `TBD_EXTERNAL`,在验收接入 前不得下发。工程自检请求示例: ```bash umask 077 cat >/run/operator/edge-self-check-task.json <<'JSON' {"task_id":"runtime-self-check-20260809","gateway_id":"11111111-1111-4111-8111-111111111111","environment":"production","network_zone":"zone-a","purpose":"quality-evaluation","classification":"evidence","task_type":"quality","contract_version":1,"deadline_at":"2026-08-09T16:00:00Z","attempt":1,"idempotency_key":"runtime-self-check-20260809","policy_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} JSON curl --fail-with-body --silent --show-error \ --header @/run/operator/human-auth.headers --header 'Content-Type: application/json' \ --data @/run/operator/edge-self-check-task.json \ https://control.example.invalid/api/datasource/edge/tasks ``` 把 deadline、gateway/policy 绑定替换为当前批准值。验收为控制面返回 task_id/digest, Agent 取得签名任务后本地事件为 `evidence`、payload 仅含 check_count/passed_count/scope/ status,event exact ACK 后 queue 待发数归零。任何其他操作在企业 handler 接入前应返回 `execution_boundary_rejected`,不能为通过测试而改成空成功。 先下载到 `/opt/dataops-edge/releases`,再验证完整 Ed25519 manifest、artifact SHA-256、deadline、artifact basename、当前/rollback version 和 key-id: ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py release-check \ --root /opt/dataops-edge \ --manifest /opt/dataops-edge/releases/release-manifest.json \ --artifact /opt/dataops-edge/releases/edge-agent-2.0.0.bin \ --trusted-key release-key-2026=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc \ --current-version 1.0.0 ``` 只有 status `verified` 的候选可进入安装。工具会把已验签 manifest、artifact 和 bundle 记录分别 fsync 后原子写入 `/opt/dataops-edge/releases/verified//`;同 digest 仅在整个受控 bundle 再验证一致时幂等返回。该目录外的“verified JSON”、验签后被篡改 artifact、manifest replay 改 digest、降级、过期、路径穿越、符号链接和任意命令均失败。安装前备份 queue 数据库、执行健康检查,再由控制面签收 installed。 正常容器升级由运维在 Agent 已停止且 queue 已 drain 后,把 `EDGE_IMAGE` 改成已验签 不可变 digest,再启动候选。`current.json` 只记录已验证 bundle,不替代镜像 digest: ```bash docker compose --env-file /opt/dataops-edge/edge.env \ -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml stop edge-gateway sed -i.bak 's#^EDGE_IMAGE=.*#EDGE_IMAGE=registry.example.invalid/dataops-edge@sha256:#' \ /opt/dataops-edge/edge.env docker compose --env-file /opt/dataops-edge/edge.env \ -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml up -d edge-gateway python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge ``` 连续 3 次 healthy、queue schema v2、签名自检 event ACK 和 `current.json`/候选 manifest 一致后才发送 installed ACK;失败立即恢复 `edge.env.bak` 并按下述回滚程序处理。不要用 可变 tag,也不要把 `release-check` 的 verified 状态当成已经安装。 发布状态机固定为 `downloaded -> verified -> candidate -> installed -> ACK`;任一步失败 转入 failed。只有新进程无法启动、120 秒无 healthy、queue schema 不是 v2 或控制面 身份绑定失败,且上个 verified bundle 仍在有效 deadline 内,才允许自动回滚。数据 语义/来源质量异常不得自动回滚,必须由数据负责人和事故指挥批准人工回滚。 安装前记录 `status`、备份 queue,停止容器,原子更新 current 指针后以相同 Compose 启动;启动成功后才发送 installed ACK。固定恢复文件为 `/opt/dataops-edge/state/rollback-intent.json`、 `/opt/dataops-edge/state/rollback-history/.json` 和 `/opt/dataops-edge/releases/current.json`。恢复检查命令: ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge sqlite3 /opt/dataops-edge/queue/edge-queue-v2.sqlite3 'PRAGMA quick_check; PRAGMA user_version;' docker compose --env-file /opt/dataops-edge/edge.env \ -f /opt/dataops-platform/deploy/edge/docker-compose.edge.yml up -d ``` 若 status 显示 rollback intent:禁止发布新版本;按 intent 中 expected current/target 重新执行完全相同的 rollback 请求。相同请求会完成/清理可恢复 intent;任何字段不同 都会冲突。不可手删 intent/history/current。恢复后必须验证 current 指针、bundle manifest/artifact SHA-256、queue quick_check/user_version=2 和连续 3 个 healthy,再 ACK。 回滚需要交互输入 `ROLLBACK`,或自动化同时提供 `--yes`、expected-current digest 和 target verified digest: ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py rollback \ --root /opt/dataops-edge --yes \ --expected-current-digest dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd \ --target-digest eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee \ --trusted-key release-key-2026=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc ``` 工具重新验证 current/target 两个受控 bundle 的完整 canonical Ed25519 manifest、可信 key-id、deadline、版本/rollback 绑定和 artifact SHA-256,随后写 rollback intent 并原子提交 `/opt/dataops-edge/releases/current.json` 指针,fsync 父目录后才返回成功;history 和 intent 使指针已提交但进程崩溃的场景可判定并幂等恢复。已有 history 回放仍须先确认 current pointer,再将遗留 intent 与本次 request digest、expected current、target 做严格等值校验;只清理完全匹配的 intent 并 fsync state 目录,异内容一律冲突。unlink/fsync 中断可由下次同请求继续恢复,且不会阻塞清理后的下一次合法回滚;备份有界为 3 份。回滚后执行健康、queue schema、control binding 和事件重放验证,再上报 outcome=rollback。若任一 bundle 被篡改、expected-current/target 不符或 fsync 失败,停止服务并按 intent/history 判定,不得强行覆盖。 ## 8. queue v1 到 v2 迁移 drain gate 当前本地 queue schema 是 v2;以下仅用于遗留 v1 主机。迁移器会拒绝仍有 pending/leased task 或未 ACK event 的 v1 数据库,因此先冻结拉取并 drain: ```bash python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge sqlite3 /opt/dataops-edge/queue/edge-queue-v1.sqlite3 \ "SELECT status,count(*) FROM edge_tasks GROUP BY status; SELECT status,count(*) FROM edge_outbound_events GROUP BY status;" sqlite3 /opt/dataops-edge/queue/edge-queue-v1.sqlite3 'PRAGMA wal_checkpoint(FULL); PRAGMA quick_check;' sqlite3 /opt/dataops-edge/queue/edge-queue-v1.sqlite3 \ "VACUUM INTO '/opt/dataops-edge/queue-backup/edge-queue-v1-drained.sqlite3';" sha256sum /opt/dataops-edge/queue-backup/edge-queue-v1-drained.sqlite3 cp -p /opt/dataops-edge/queue-backup/edge-queue-v1-drained.sqlite3 \ /opt/dataops-edge/queue-migration/edge-queue-v2-candidate.sqlite3 EDGE_QUEUE_CANDIDATE=/opt/dataops-edge/queue-migration/edge-queue-v2-candidate.sqlite3 \ /opt/dataops-platform/.venv/bin/python -c \ 'import os; from app.core.edge_gateway.queue import SqliteEdgeQueue; SqliteEdgeQueue(os.environ["EDGE_QUEUE_CANDIDATE"])' sqlite3 /opt/dataops-edge/queue-migration/edge-queue-v2-candidate.sqlite3 \ 'PRAGMA quick_check; PRAGMA foreign_key_check; PRAGMA user_version;' ``` 验收必须依次返回 `ok`、无 foreign-key 行、`2`;随后在候选副本执行幂等 replay、 cancel、outcome/event ACK 用例,停旧容器后才原子改名为 `edge-queue-v2.sqlite3` 并启动新 verified release。复制迁移期间禁止写 v1;不得直接 在唯一副本原地迁移。失败时停止候选进程、保留失败摘要,删除候选文件并从 SHA-256 已核对的 v1 drained 备份恢复;旧 verified release 只读该恢复文件。 ## 9. 备份、恢复与事故 - 备份:queue、verified releases、public signing keys、非秘密配置和证书链;credential/private key 使用企业秘密/密钥备份制度,不进入普通备份。 - 恢复:新主机先重建 0700/0600 权限,验证证书/CRL/签名公钥,再恢复 queue;先 reconcile 后启动执行。 - 事故:身份失败、CRL 命中、原始数据出域尝试、签名失败、replay conflict、queue 损坏或 rollback 失败均停止对应流,保存 digest/计数/时间/版本证据,联系网络、安全和平台负责人。 - 验收关闭:工程证据不替代企业网络抓包、批准来源、数据边界审计和 security/legal 签字;这些仍为 `TBD_EXTERNAL`。 ### 9.1 retention、legal hold 与 artifact cleanup 清理前由数据负责人提供 retention policy digest;存在 legal hold 的 task/artifact 不得 进入 deleting。先用只读查询导出候选 digest(不导出 artifact_ref),审批后由 agent 正常 cleanup worker 执行;禁止用 `rm` 绕过状态机: ```bash sqlite3 -readonly /opt/dataops-edge/queue/edge-queue-v2.sqlite3 \ "SELECT artifact_digest,classification,retention_until,cleanup_status FROM edge_local_artifacts WHERE cleanup_status IN ('pending','failed') AND julianday(retention_until)<=julianday('now') ORDER BY retention_until LIMIT 100;" python3 /opt/dataops-platform/scripts/edge_gateway_admin.py status --root /opt/dataops-edge ``` 注意:queue schema v2 没有可由操作者直接修改的 `legal_hold` 列,且不提供人工强制删除 命令。企业 legal-hold 服务接入、批准记录签名和 cleanup worker 的 hold 消费仍属于 `security_legal=TBD_EXTERNAL`;接入前一旦出现 hold,必须停止 Agent 以阻止自动 cleanup, 由数据/法务负责人决定延长 retention 后重新生成批准任务,不能直接改 SQLite 或运行 `rm`。因此本节是候选只读核对,不是“已完成 legal hold 产品能力”的声明。 每个删除必须形成 canonical delete receipt,至少含 artifact_digest、policy_digest、 approved_by、deleted_at、result 和 receipt_digest,并由 outbox 发送 ACK;本地表只有 receipt ACK 后才能保持 deleted。cleanup 中断时,`deleting` lease 到期后由同一 artifact digest 重试:文件已不存在则生成幂等 delete receipt,不得改写 digest; 失败转 failed 并保留分类化错误码。恢复验收为磁盘水位下降、receipt 已 ACK、没有 legal hold 对象被删。legal hold 解除必须有新批准记录,不能直接改 SQLite。 ### 9.2 queue 与 release 备份恢复 在停止新 pull、active lease=0 后执行 SQLite 在线一致副本和完整性校验;verified release、config、public signing keys、CA/CRL 分别归档并计算摘要: ```bash sqlite3 /opt/dataops-edge/queue/edge-queue-v2.sqlite3 \ "VACUUM INTO '/opt/dataops-edge/backup/edge-queue-v2.sqlite3';" sqlite3 /opt/dataops-edge/backup/edge-queue-v2.sqlite3 \ 'PRAGMA quick_check; PRAGMA foreign_key_check; PRAGMA user_version;' sha256sum /opt/dataops-edge/backup/edge-queue-v2.sqlite3 tar -czf /opt/dataops-edge/backup/public-state.tar.gz \ -C /opt/dataops-edge config signing certificates releases state/rollback-history sha256sum /opt/dataops-edge/backup/public-state.tar.gz ``` 恢复到新建的空专用 leaf,先验证两份 SHA-256,再执行 quick_check/foreign_key_check/ user_version=2、签名公钥和 CRL freshness;恢复 current 指针前重新执行 bundle 验签。 启动时先 reconcile 后 pull。任一校验失败则保持停机、回退到上一份已验证备份;不得 在损坏原件上修表。credential/private key 由企业密钥恢复流程单独注入,绝不包含在 `public-state.tar.gz`。 ## 10. PostgreSQL 三身份执行顺序 职责固定:role-init 仅一次性创建/授予角色,需要 DBA/CREATEROLE;migrator 是 `dataops_edge_evidence_owner` 成员、负责 Alembic/DDL、无 CREATEROLE;runtime 只持 `dataops_app_runtime` DML/EXECUTE,不是对象 owner。三个连接串写入 root-only `0600` 环境文件,禁止命令行明文。按顺序执行: ```bash set -a . /etc/dataops-platform/dataops.env set +a /opt/dataops-platform/.venv/bin/python -m app.core.edge_gateway.runtime_roles unset DB_ROLE_INIT_DATABASE_URL MIGRATION_DATABASE_URL="$MIGRATION_DATABASE_URL" \ /opt/dataops-platform/.venv/bin/alembic -c /opt/dataops-platform/alembic.ini upgrade head MIGRATION_DATABASE_URL="$MIGRATION_DATABASE_URL" \ /opt/dataops-platform/.venv/bin/alembic -c /opt/dataops-platform/alembic.ini current ``` `DB_ROLE_INIT_DATABASE_URL` 只交给一次性 role-init,`MIGRATION_DATABASE_URL` 只交给 Alembic 子进程,常驻 backend/runner 只获得 `DATABASE_URL`。用于验收的 `/etc/postgresql/pg_service.conf` 仅写 host/port/dbname/user,密码放 `0600` passfile; 分别执行 `PGSERVICE=dataops_migrator psql -X` 和 `PGSERVICE=dataops_runtime psql -X`,不能共用 owner 登录。 `current` 必须为完整 revision `20260809_479 (head)`。随后分别用 migrator/runtime 会话核验职责(以下 SQL 在各自 `psql` 会话执行,连接由 0600 service/passfile 提供): ```sql SELECT current_user, has_database_privilege(current_user,current_database(),'CREATE') AS db_create, has_schema_privilege(current_user,'public','CREATE') AS schema_create, pg_has_role(current_user,'dataops_edge_evidence_owner','member') AS evidence_owner, pg_has_role(current_user,'dataops_app_runtime','member') AS app_runtime; SELECT rolcreaterole, rolsuper FROM pg_roles WHERE rolname=current_user; ``` 验收:role-init 完成后 credential 回收;migrator evidence_owner=true、schema_create=true、 rolcreaterole=false、rolsuper=false;runtime app_runtime=true、schema_create=false、 rolcreaterole=false、rolsuper=false,且不能写 signing authority。任一步失败停止应用, 由 DBA 修 GRANT 后从 role-init/migration gate 重跑;禁止给 runtime 临时 owner/superuser。 ## 11. 双向 TLS 与 CRL 更新/恢复 控制面 Nginx 用 client CRL 撤销 Edge 客户端;Edge Agent 用 server CRL 验证控制面 服务端,二者不能互换。安全人员先验证 issuer、签名、thisUpdate/nextUpdate,再在同一 文件系统写 `.next`、fsync 并原子 rename 只读源。Agent 每次请求前核对 CRL digest, 变化时关闭旧连接池并用新 CRL 重建 SSLContext;无需重启即可拒绝新撤销,CRL 过期即 fail-stop,且 SSLContext 必须保持 `CRL_CHECK_LEAF`: ```bash openssl crl -in /opt/dataops-edge/certificates/control-plane-server.crl.pem -noout -issuer -lastupdate -nextupdate openssl verify -CAfile /opt/dataops-edge/certificates/ca.pem -crl_check \ -CRLfile /opt/dataops-edge/certificates/control-plane-server.crl.pem \ /opt/dataops-edge/certificates/control-plane-server.pem openssl crl -in /etc/nginx/edge-client.crl.pem -noout -issuer -lastupdate -nextupdate install -m 0644 /run/operator/control-plane-server.crl.pem.next \ /opt/dataops-edge/certificates/control-plane-server.crl.pem.next sync /opt/dataops-edge/certificates/control-plane-server.crl.pem.next mv /opt/dataops-edge/certificates/control-plane-server.crl.pem.next \ /opt/dataops-edge/certificates/control-plane-server.crl.pem install -m 0644 /run/operator/edge-client.crl.pem.next /etc/nginx/edge-client.crl.pem.next sync /etc/nginx/edge-client.crl.pem.next mv /etc/nginx/edge-client.crl.pem.next /etc/nginx/edge-client.crl.pem nginx -t nginx -s reload ``` 验收:新 client CRL 中撤销证书无法握手;有效证书仍通过;Edge health 中 CRL nextUpdate 新鲜且控制面握手成功;工程测试已证明撤销当前服务端证书后下一次请求立即 失败,不依赖容器重启。更新前保留一份同样已验签且未过期的 `.previous`;更新失败时 原子恢复该文件,Nginx 执行 `nginx -t && nginx -s reload`,Edge 在下一次请求自动重建 连接池; 若上一份已过期则保持 fail-stop,不能关闭 CRL 检查。企业 CA/CRL 实际轮换、Nginx/ Agent reload 和抓包证据仍是 `TBD_EXTERNAL`。