Skip to content

XHIM Server 发布审计门禁

本 Runbook 适用于服务端源码、Migration、Compose 或公开接口变化。一次服务端 任务只有由发布人员或受控编排系统实际执行发布,并取得以下证据后,才可标记为 “已部署且审计通过”:

  1. 现网 Runtime Observation;
  2. 加密 Secret/数据库备份、恢复演练和 Migration Readiness;
  3. 受保护 CI 生成的不可变镜像、SBOM 与 Image Provenance;
  4. Clean Commit + Secret 连续性 Preflight;
  5. 受控 server-onlyserver-and-config 发布;
  6. 发布后的 Runtime Observation;
  7. Revision/Image Digest、健康、公开配置、账号链路与已认证 WSS Upgrade Postflight;
  8. 目标端真机 SDK 消息回归。

CI 中的 server-release-audit-gate 只测试门禁代码和负向契约,不会自动登录、 修改、重启或部署客户服务器。preflight / postflightpassed 只表示提供 给脚本的证据通过审计,不表示发布动作已经发生。没有目标主机生成的 Runtime Observation、实际发布记录和 Postflight 证据时,状态必须写成“代码完成,服务端 未部署”。

安全边界

server_release_gate.py

  • observe-runtime 只读取部署文件、Docker Compose/Inspect 的受限字段,并在 PostgreSQL 容器中执行固定的只读 SELECT version FROM xhim_schema_migrations ORDER BY version
  • bootstrap-continuity 只创建一次不可覆盖的连续性锚点;
  • preflight 不修改服务器;
  • postflight 只读健康/配置并真实执行携带 Bearer Token 的 WebSocket Upgrade; 当服务端公开声明 development_phone_auth_enabled=true 时,会自动创建一个合成 Development 账号、强制验证 register → password-login,再使用该登录 Token 验证 Upgrade;
  • rollback-preflight 只生成一小时有效的应用回滚授权。

脚本不会 SSH、执行 Docker 更新、恢复数据库、执行 Migration 或轮换应用 Secret。它也不会把 .env、手机号、密码、Token、数据库 URL 或明文 Secret 写入证据。

1. 先采集现网,禁止套用示例路径

server-release.example.json 不是可直接部署的配置。先在目标主机用只读命令 确认真实状态:

bash
pwd -P
docker compose ls
docker volume ls
docker ps \
  --filter label=com.docker.compose.project \
  --format '{{.ID}} {{.Names}} {{.Image}}'

随后确认:

  • Deployment Root、Shared Directory、Releases Directory;
  • 当前 .env.env.release、Compose、deploy/Caddyfile 的真实绝对路径;
  • Compose Project 与 serverpostgresgateway Service;
  • PostgreSQL、Media、Caddy 的实际 Docker Volume Name。

当前某个环境即使采用 /www/wwwroot/xhim-server/shared/.env + releases/,也必须现场验证,不能把 该路径当成其他客户环境的默认值。将观测值写入客户专属的 Deployment Manifest; 该文件放受控配置库,不提交客户域名或路径到 SDK 源码。

部署布局支持 Release Directory:

text
<observed-deployment-root>/
├── shared/
│   ├── .server-continuity-anchor.json
│   ├── current -> config-sets/<active-release-id>
│   └── config-sets/
│       └── <release-id>/
│           ├── .env
│           ├── .env.release
│           ├── compose.yaml
│           └── deploy/
│               └── Caddyfile
└── releases/
    └── <version>-<full-commit>/

Manifest 中 .env.env.releasecompose.yaml 必须位于同一个 Config Set 目录,gateway_config_file 必须是该目录下的 deploy/Caddyfile。四个文件 任一跨目录都会被门禁拒绝。Manifest 还必须显式声明:

  • continuity_anchor_file:位于 Shared Directory、文件名固定为 .server-continuity-anchor.json 的一次性连续性锚点;
  • development_phone_auth_required:Development 演示环境必须为 true; Production 必须为 false。为 true 时,环境配置、公开能力和真实 register → password-login 任一步缺失都会拒绝通过。

.env.release 固定只含:

dotenv
XHIM_SERVER_IMAGE=registry.example/xhim/server@sha256:<64-hex-digest>
XHIM_SERVER_VERSION=VERSION
XHIM_SERVER_COMMIT=FULL_COMMIT

若既有环境没有 .env.release、OCI Version/Revision Label 或不可追溯的旧镜像, 先完成一次受审批的 Baseline Bootstrap;在基线建立前不能声称支持受控应用 回滚。Production 的 XHIM_SERVER_IMAGE 必须是 Registry image@sha256:<digest>,Runtime Observation 的 RepoDigests 必须非空并包含该 摘要。Tag、空 RepoDigests 或只有本机 Image ID 均不构成可销售环境的发布证据。 Production 的 .env 还必须把 XHIM_POSTGRES_IMAGEXHIM_GATEWAY_IMAGE 配置为不可变 image@sha256;门禁会分别读取三个运行镜像 的 RepoDigests,并要求 Container Image Reference 命中对应摘要。

POSTGRES_PASSWORD=xhim-local-only 只允许本地 Development。Production 必须由 Secret Manager 注入至少 20 字符的独立密码,而且 Server 的 XHIM_DATABASE_URL 中用户、Host、Database 与解码后的密码必须和 PostgreSQL 容器一致。给 Compose 写一个“看起来很强”的新密码但未轮换数据库角色,或 Server 仍使用旧密码时都会失败;既有数据库凭证轮换必须走独立受控流程。

2. Secret 指纹 Key

门禁需要一个独立、持久的 Fingerprint Key。它应由 Secret Manager/KMS 注入为 权限 0600 的临时文件,至少 32 字节。该 Key 只用于:

  • HMAC-SHA256 签署发布证据;
  • 计算持久 Secret 指纹;
  • 比较发布前后 Secret 连续性。

它不是 XHIM Token、Cursor、数据库或 Push 凭证,不得每次发布重新生成。 证据只保存 HMAC 指纹。尤其 XHIM_CURSOR_KEY_BASE64 指纹变化会立即阻断发布, 防止“新账号注册成功”掩盖旧手机号摘要、Cursor 或旧会话全部失效。

3. 发布前 Runtime Observation

bash
XHIM_DEPLOYMENT_MANIFEST=/secure/xhim/deployment.json
XHIM_FINGERPRINT_KEY=/run/secrets/xhim-release-fingerprint-key
XHIM_EVIDENCE_DIR=/secure/xhim/release-evidence

install -d -m 0700 "$XHIM_EVIDENCE_DIR"

scripts/deploy/server_release_gate.py observe-runtime \
  --deployment-manifest "$XHIM_DEPLOYMENT_MANIFEST" \
  --fingerprint-key-file "$XHIM_FINGERPRINT_KEY" \
  --output "$XHIM_EVIDENCE_DIR/runtime-before.json"

Observation 会绑定:

  • Deployment/Shared/Releases 和四个部署文件;
  • 四个部署文件经过父目录 Symlink 解析后的真实 Release 路径;
  • Compose Project、Service、逻辑 Volume 与实际 Runtime Volume Name;
  • 当前 .env.release 声明;
  • serverpostgresgateway 三个 Service 的 Container ID、Image ID、 Image Reference、RepoDigests、唯一 Compose Network 和完整 Mount Type/Source/Destination/RW;Server 额外 Host Bind 或外部 Network 会直接失败;
  • 三个 Container 使用的 Image ID Content Digest 与非空 RepoDigests;
  • 实际 OCI Version/Revision Label;
  • 在线 xhim_schema_migrations 的完整有序版本集合及其 SHA-256;
  • current_database() 与 PostgreSQL Cluster system_identifier 组成的数据库 身份 SHA-256;备份 Metadata 必须与该身份一致,避免拿同版本/同 Schema 的 另一客户数据库冒充现网备份;
  • .env 文件 HMAC 与持久 Secret HMAC 指纹。

它不会执行不受限的 docker inspect,避免把 Container Environment 输出到日志; 数据库观测也只执行上述固定 SELECT,不会输出数据库 URL 或业务数据。

3.1 仅一次的 Continuity Bootstrap

新环境在第一次普通 Preflight 前,必须用已审批、已签名的 Runtime Observation 建立一次连续性锚点:

bash
scripts/deploy/server_release_gate.py bootstrap-continuity \
  --deployment-manifest "$XHIM_DEPLOYMENT_MANIFEST" \
  --fingerprint-key-file "$XHIM_FINGERPRINT_KEY" \
  --runtime-observation-file "$XHIM_EVIDENCE_DIR/runtime-before.json" \
  --bootstrap-approval-id CHANGE-APPROVAL-2026-001 \
  --output \
  /REPLACE_WITH_OBSERVED_DEPLOYMENT_ROOT/shared/.server-continuity-anchor.json

输出必须与 Manifest 的 continuity_anchor_file 完全相同。锚点应由发布账号以 0600 创建,并进入不可覆盖、可审计的备份;文件已存在、Symlink、审批号缺失 或 Fingerprint Key 改变时 Bootstrap 必须失败。它不能作为“修复连续性失败”的 重试开关,也不能在 Cursor、签名或数据库等持久身份变化后重新初始化。合法的 凭证轮换必须走单独的轮换方案、双 Key 兼容期和审批,不属于普通发布。

本地 O_EXCL 只能防覆盖,无法阻止拥有目标主机 Root 权限的人删除锚点。商用 环境必须把锚点 Hash、审批号和第一次 Postflight 写入外部不可变/WORM 审计存储, 并让后续发布强制提供上一份 Postflight。锚点或外部 Adoption Receipt 任一缺失时 必须停止发布并人工核查,禁止通过“再跑一次 bootstrap”恢复绿色状态;本脚本不 把普通磁盘文件伪装成防 Root 删除的可信账本。

4. Preflight

本节先说明 Preflight 接口,但真实执行顺序必须是: Runtime Observation → 第 5 节备份/恢复与 Migration Readiness → 第 6 节镜像 构建/推送与 Image Provenance → Preflight → 第 7 节发布。不得先生成空白证据 通过 Preflight,再补做备份或构建。

4.1 三份必需的受保护候选文件

每次发布都必须准备以下三个绝对路径、0600、非 Symlink、未被 Git 跟踪的 Regular File。它们只能由 Protected CI Runner 或受控运维作业生成和传递,不能 由开发者手填后上传普通 Artifact:

  1. candidate.env.release:严格的 Dotenv 文件,只含 XHIM_SERVER_IMAGEXHIM_SERVER_VERSIONXHIM_SERVER_COMMIT。它由 Protected Image Job 在 Registry 推送、摘要解析完成后生成;
  2. image-provenance.json:Schema com.xihansoftware.xhim.server-image-provenance.v1。它由 Protected CI Image Job 根据 Clean Commit、git rev-parse <commit>:server、Registry Digest、实际 Image ID、SBOM SHA-256 和镜像签名验证结果生成;
  3. migration-readiness.json:Schema com.xihansoftware.xhim.server-migration-readiness.v1。它由受保护的数据库 运维作业根据本次 Runtime Observation 的在线 Migration 集合、候选 Migration Tree、加密备份校验、隔离 Restore Drill 和 N/N-1 双向兼容结果生成。

后两份 JSON 必须用第 2 节同一个受保护 Fingerprint Key 对 Canonical JSON 做 HMAC-SHA256,写入 evidence_hmac_sha256;Preflight 会验签并把证据 Hash 写入 自己的签名结果。signature_verified=true 还表示 Registry 中候选镜像的供应链 签名已经由 Protected Image Job 独立验证,它不能由发布人员手工改成 true

Image Provenance 的必需结构是:

json
{
  "schema": "com.xihansoftware.xhim.server-image-provenance.v1",
  "schema_version": 1,
  "product": "XHIM",
  "status": "attested",
  "created_at_utc": "2026-07-29T00:00:00Z",
  "source": {
    "commit": "<full-clean-commit>",
    "version": "<semver>",
    "server_tree": "<git-tree-object-id>"
  },
  "image": {
    "reference": "registry.example/xhim/server@sha256:<64-hex>",
    "repository_digest": "registry.example/xhim/server@sha256:<same-64-hex>",
    "image_id": "sha256:<64-hex>",
    "sbom_sha256": "<64-hex>",
    "signature_verified": true
  },
  "evidence_hmac_sha256": "<protected-job-signature>"
}

Migration Readiness 的必需结构是:

json
{
  "schema": "com.xihansoftware.xhim.server-migration-readiness.v1",
  "schema_version": 1,
  "product": "XHIM",
  "status": "passed",
  "checked_at_utc": "2026-07-29T00:00:00Z",
  "source_commit": "<full-clean-commit>",
  "candidate_files": {
    "0001_core.sql": "<file-sha256>"
  },
  "candidate_tree_sha256": "<canonical-tree-sha256>",
  "baseline_applied_migrations": [
    "0001_core.sql"
  ],
  "baseline_schema_sha256": "<online-migration-set-sha256>",
  "backup": {
    "bundle_directory": "/secure/.../postgres-backup",
    "metadata_sha256": "<actual-backup-metadata-sha256>",
    "encrypted_artifact_file": "/secure/.../postgres-backup.tar.age",
    "encrypted_artifact_sha256": "<actual-encrypted-file-sha256>",
    "encryption_report_file": "/secure/.../backup-encryption-report.json",
    "encryption_report_sha256": "<actual-report-sha256>"
  },
  "restore_drill": {
    "evidence_file": "/secure/.../restore-drill-report.json",
    "evidence_sha256": "<restore-report-sha256>"
  },
  "compatibility": {
    "declaration_file": "/secure/.../postgres-compatibility.json",
    "declaration_sha256": "<declaration-sha256>",
    "test_report_file": "/secure/.../n-minus-one-run-report.json",
    "test_report_sha256": "<actual-run-report-sha256>"
  },
  "evidence_hmac_sha256": "<protected-job-signature>"
}

Readiness 必须绑定完整候选 Migration 文件 Hash 和 Observation 中的完整在线集合, 且生成时间不得超过七天。门禁会实际打开以上绝对路径:备份目录必须为私有真实 目录且只含 xhim-postgres.dumpbackup-metadata.jsonSHA256SUMS; 大 Dump 和加密制品采用流式 SHA-256,不会整体载入内存;JSON、权限、实际 Hash、 基线 Version/Schema、恢复表集合和候选 Version/Schema 任一不一致都会失败。

postgres-compatibility.json 只是带有效期的兼容声明,不能冒充运行证明。 n-minus-one-run-report.json 必须是 Schema com.xihansoftware.xhim.postgres-n-minus-one-verification.v1 的实际受保护运行 报告,绑定本次 Commit、N/N-1 版本、候选 Schema,并逐项记录 N、N-1 读写、 Ready、已认证 WSS 与消息幂等回归。没有该报告时正式 Preflight 必须失败。 以上 JSON 只是 Schema 说明,不是允许复制修改的发布模板;正式文件必须来自 对应的 Protected CI/运维作业。

加密验证报告必须是 Canonical JSON,且 method 只能为 agekms-envelope

json
{
  "schema": "com.xihansoftware.xhim.postgres-backup-encryption-verification.v1",
  "schema_version": 1,
  "product": "XHIM",
  "status": "verified",
  "verified_at_utc": "2026-07-29T00:00:00Z",
  "method": "age",
  "storage_reference": "vault-or-object-version-id",
  "backup_bundle_sha256": "<canonical-three-file-set-sha256>",
  "encrypted_artifact_sha256": "<actual-encrypted-file-sha256>"
}

N/N-1 实际运行报告的 checks 必须精确包含以下五项且全部为 true

json
{
  "schema": "com.xihansoftware.xhim.postgres-n-minus-one-verification.v1",
  "schema_version": 1,
  "product": "XHIM",
  "status": "passed",
  "checked_at_utc": "2026-07-29T00:00:00Z",
  "source_commit": "<full-clean-commit>",
  "from_server_version": "<baseline-semver>",
  "to_server_version": "<candidate-semver>",
  "database_schema_version": "<candidate-latest-migration.sql>",
  "checks": {
    "release_n_read_write": true,
    "release_n_minus_one_read_write": true,
    "ready": true,
    "authenticated_websocket": true,
    "message_idempotency": true
  }
}

普通发布必须引用上一次成功 Postflight,使 Secret 和现网镜像形成连续链:

bash
XHIM_VERSION=0.1.0-dev.4
XHIM_COMMIT="$(git rev-parse HEAD)"
XHIM_CANDIDATE_RELEASE_ENV=/run/xhim-release/candidate.env.release
XHIM_IMAGE_PROVENANCE=/run/xhim-release/image-provenance.json
XHIM_MIGRATION_READINESS=/run/xhim-release/migration-readiness.json

scripts/deploy/server_release_gate.py preflight \
  --repository-root "$PWD" \
  --expected-commit "$XHIM_COMMIT" \
  --expected-version "$XHIM_VERSION" \
  --deployment-manifest "$XHIM_DEPLOYMENT_MANIFEST" \
  --fingerprint-key-file "$XHIM_FINGERPRINT_KEY" \
  --baseline-runtime-observation \
  "$XHIM_EVIDENCE_DIR/runtime-before.json" \
  --previous-postflight-file \
  "$XHIM_EVIDENCE_DIR/previous-postflight.json" \
  --deployment-mode server-only \
  --candidate-release-env-file "$XHIM_CANDIDATE_RELEASE_ENV" \
  --image-provenance-file "$XHIM_IMAGE_PROVENANCE" \
  --migration-readiness-file "$XHIM_MIGRATION_READINESS" \
  --output "$XHIM_EVIDENCE_DIR/preflight.json"

首次引入门禁没有 Previous Postflight 时,只能引用第 3.1 节已经创建的一次性 Continuity Anchor;普通 Preflight 不提供重新初始化连续性的参数。第一次成功 Postflight 之后,后续发布必须引用上一份成功 Postflight。

模式:

  • server-only:候选 Compose Hash 必须与现网一致,只允许更新 Server Image。 发布前后 PostgreSQL/Gateway 容器 ID 和 Image ID、Compose Network ID/集合, 以及 Server/PostgreSQL/Gateway 的 Mount Source/Destination/RW 属性必须完全 不变;任何依赖容器被重建、镜像漂移、Network 重建或 Mount 漂移都拒绝通过;
  • server-and-config:允许把仓库内候选 server/compose.yaml 作为本次受控 Compose 变更,并通过 --candidate-runtime-env-file 接收 Secret Manager 导出的 0600、未被 Git 跟踪的候选运行配置。Preflight 同时记录旧/新 Compose Hash 与 .env HMAC,Postflight 强制验证新值。

普通 server-and-config 只允许修改以下运行参数;新增、删除或修改其他任何 环境变量都必须拒绝,并改走对应的网络、身份、凭证、存储或数据迁移专项变更:

text
XHIM_REQUEST_RATE
XHIM_REQUEST_BURST
XHIM_MAX_WEBSOCKETS
XHIM_SHUTDOWN_TIMEOUT
XHIM_ENDPOINT_LIFETIME
XHIM_MINIMUM_CLIENT_PROTOCOL_VERSION
XHIM_MULTI_LOGIN_POLICY
XHIM_MAX_SESSIONS_PER_USER
XHIM_MAX_BURN_AFTER_READ
XHIM_BURN_WORKER_BATCH_SIZE
XHIM_BURN_WORKER_IDLE_INTERVAL
XHIM_MEDIA_AUTH_LIFETIME
XHIM_PUSH_REQUEST_TIMEOUT
XHIM_PUSH_BATCH_SIZE
XHIM_PUSH_MAX_ATTEMPTS
XHIM_PUSH_LEASE_DURATION
XHIM_PUSH_IDLE_INTERVAL
XHIM_PUSH_MAX_DEVICES_PER_USER
XHIM_PUSH_DELIVERY_CONCURRENCY
XHIM_WEBHOOK_REQUEST_TIMEOUT
XHIM_WEBHOOK_BATCH_SIZE
XHIM_WEBHOOK_MAX_ATTEMPTS
XHIM_WEBHOOK_LEASE_DURATION
XHIM_WEBHOOK_IDLE_INTERVAL
XHIM_WEBHOOK_BASE_BACKOFF
XHIM_WEBHOOK_MAX_BACKOFF
XHIM_WEBHOOK_DELIVERY_CONCURRENCY
XHIM_RTC_TOKEN_LIFETIME

尤其域名/Public URL、数据库地址、对象存储、环境类型、Development Login、 签名 Key ID、Verify Keys、Token Issuer/Audience 和所有持久 Secret 都不在普通 Allowlist。凭证或身份轮换是另一套审批流程,不能夹带在普通配置发布中。

Preflight 拒绝 Dirty Worktree、Commit/Version 错配、过期或被篡改的 Runtime Observation、路径/Project/Volume 漂移、Secret 指纹断链以及未审批的 Compose 变化;候选 .env.release 与 Image Provenance 不一致、Image/SBOM/签名来源 无法验签,或 Migration Readiness 与候选 Tree/在线 Schema/备份恢复/N/N-1 证据不一致时同样 Fail Closed。

5. 加密备份

.env 禁止明文复制到 /var/backups 或普通 CI Artifact。先配置一个经过审批的 Age Recipient,或使用客户 Vault/KMS 的版本化 Secret Backup。Age 示例:

bash
XHIM_RUNTIME_ENV='REPLACE_WITH_OBSERVED_RUNTIME_ENV_PATH'
XHIM_RELEASE_ENV='REPLACE_WITH_OBSERVED_RELEASE_ENV_PATH'
XHIM_DEPLOYED_COMPOSE='REPLACE_WITH_OBSERVED_COMPOSE_PATH'
XHIM_GATEWAY_CONFIG='REPLACE_WITH_OBSERVED_CADDYFILE_PATH'
XHIM_ENCRYPTED_BACKUP_ROOT='REPLACE_WITH_ENCRYPTED_BACKUP_MOUNT'
XHIM_RELEASE_ID='VERSION-FULL_COMMIT'
XHIM_BACKUP_SET="$XHIM_ENCRYPTED_BACKUP_ROOT/$XHIM_RELEASE_ID"
XHIM_AGE_RECIPIENTS=/secure/xhim/backup-recipients.txt

test ! -e "$XHIM_BACKUP_SET"
install -d -m 0700 "$XHIM_BACKUP_SET"

age --encrypt \
  --recipients-file "$XHIM_AGE_RECIPIENTS" \
  --output "$XHIM_BACKUP_SET/runtime-env.age" \
  "$XHIM_RUNTIME_ENV"

install -m 0600 \
  "$XHIM_RELEASE_ENV" \
  "$XHIM_BACKUP_SET/.env.release"
install -m 0600 \
  "$XHIM_DEPLOYED_COMPOSE" \
  "$XHIM_BACKUP_SET/compose.yaml"
install -d -m 0700 "$XHIM_BACKUP_SET/deploy"
install -m 0600 \
  "$XHIM_GATEWAY_CONFIG" \
  "$XHIM_BACKUP_SET/deploy/Caddyfile"

XHIM_ENCRYPTED_BACKUP_ROOT 必须是加密、访问受控且有保留策略的目标。若使用 Vault/KMS,不生成 runtime-env.age,而是把版本化 Secret Reference 和审计 ID 写入发布记录。

PostgreSQL Dump 同样包含敏感数据,只能写入上述加密目标:

bash
export XHIM_POSTGRES_SOURCE_URL='<由 Secret Manager 注入>'

server/scripts/postgres/backup.sh \
  --output-dir "$XHIM_BACKUP_SET/postgres" \
  --server-version 'CURRENT_ONLINE_VERSION'

server/scripts/postgres/verify_backup.sh \
  --bundle-dir "$XHIM_BACKUP_SET/postgres" \
  --expected-server-version 'CURRENT_ONLINE_VERSION' \
  --expected-schema-version 'CURRENT_ONLINE_MIGRATION'

tar --create --file=- \
  --directory "$XHIM_BACKUP_SET" postgres |
  age --encrypt \
    --recipients-file "$XHIM_AGE_RECIPIENTS" \
    --output "$XHIM_BACKUP_SET/postgres-backup.tar.age"

正式环境还必须在隔离空库完成 Restore Drill。备份、Hash、TOC、恢复演练任一 失败即停止发布。受保护作业还必须对真实 postgres-backup.tar.age 做解密校验, 把三文件集合 Hash、加密文件 Hash、Method 和不可变 Storage Reference 写入上节 加密验证报告;只填写 encrypted=true 或虚构 Hash 不能通过门禁。

5.1 Migration 与 Schema 证据

发布记录必须绑定同一 Commit 的完整 Migration Tree,而不是只记录“最新文件名”:

bash
find server/internal/adapters/postgres/migrations \
  -type f -name '*.sql' -print0 |
  sort -z |
  xargs -0 sha256sum \
  >"$XHIM_EVIDENCE_DIR/migration-tree.sha256"

同时保存并交叉验证:

  1. 发布 Commit 中按顺序、无缺号/重名的 Migration Tree 与整体 Hash;
  2. 备份前、Migration 后和 Postflight 时从在线库读取的真实 Schema Version;
  3. backup-metadata.jsonSHA256SUMS、TOC 校验及隔离空库 Restore Drill 报告;
  4. postgres-compatibility.json 中 Release N、N-1、目标 Schema 和未过期 兼容窗口;
  5. rolling_upgrade_preflight.sh 结果,以及 N 和 N-1 在目标 Schema 上实际 完成读写、Ready、WSS 与消息幂等回归的证据。

Migration Tree 或在线 Schema 与候选 Commit 不一致、恢复演练缺失、N/N-1 证据缺失或兼容窗口过期时,禁止更新任何 Server 副本。即使本次没有新增 Migration,也要记录“Tree 未变化”和发布前后在线 Schema 相同的证据。具体备份、 恢复及兼容性工具见 server/scripts/postgres/README.md。 受保护数据库运维作业把这些来源证据的 SHA-256 和双向兼容结论写入第 4.1 节的 migration-readiness.json 并完成 HMAC 签名;普通 CI Job 不能自行制造 status=passed

6. 构建不可变镜像

bash
XHIM_IMAGE_TAG="registry.example/xhim/server:${XHIM_VERSION}-${XHIM_COMMIT}"

docker build --pull \
  --build-arg XHIM_VERSION="$XHIM_VERSION" \
  --build-arg XHIM_COMMIT="$XHIM_COMMIT" \
  --tag "$XHIM_IMAGE_TAG" \
  server

推送后从 Registry 读取不可变摘要并用于部署:

bash
docker push "$XHIM_IMAGE_TAG"
XHIM_IMAGE="$(
  docker image inspect "$XHIM_IMAGE_TAG" \
    --format '{{index .RepoDigests 0}}'
)"
test -n "$XHIM_IMAGE"

Production .env.release 只能写上述 image@sha256:<digest>latest、只有 Tag、 空 RepoDigests、只有本机 Image ID,或缺少 OCI Revision Label 的镜像不能通过 Postflight。

Protected Image Job 随后必须验证镜像供应链签名、生成或验证 SBOM、读取实际 Image ID 与 Repo Digest,并按第 4.1 节生成 HMAC 签名的 image-provenance.json 和严格三字段的 candidate.env.release。二者必须引用 同一个 image@sha256;在这两份受保护文件生成前不能执行 Preflight。

门禁会校验 Provenance 的 HMAC、Commit/Server Tree、Image Digest/Image ID 与 SBOM Hash,但不会在目标主机重新登录 Registry、运行 Cosign 或解析整份 SBOM。 因此 signature_verified=true 是受信 Protected Image Job 的断言,不是本脚本 本地完成的独立验签。该 Job 的权限必须与普通开发者隔离,并把实际 SBOM、签名 验证报告、Issuer/Subject、工具版本和 Registry 不可变引用保存到供应链审计存储; 缺少这些外部制品时只能说“Provenance 断言已验签”,不能声称供应链独立审计完成。

7. 受控发布

server-only 不修改 Runtime Env 或 Compose;server-and-config 只能安装 Preflight 已绑定的候选 Runtime Env 和 Compose。两种模式都不能轮换 Secret 或 身份,也不能用三个彼此独立的 mv 制造“部分新、部分旧”的配置。

.env.env.releasecompose.yamldeploy/Caddyfile 放入同一个 不可变版本化 Config Set。先在部署锁内完成候选目录写入、权限与 Preflight Hash 校验,再用同文件系统上的 Rename 原子切换 shared/current Symlink。Manifest 的四个部署文件路径都必须通过 shared/current/ 指向同一个 Config Set:

bash
XHIM_SHARED='REPLACE_WITH_OBSERVED_SHARED_DIRECTORY'
XHIM_RELEASE_ID="${XHIM_VERSION}-${XHIM_COMMIT}"
XHIM_CONFIG_SET="$XHIM_SHARED/config-sets/$XHIM_RELEASE_ID"
XHIM_CURRENT_NEXT="$XHIM_SHARED/.current.next"
XHIM_CANDIDATE_RUNTIME_ENV=/run/secrets/xhim-candidate-runtime-env
XHIM_CANDIDATE_RELEASE_ENV=/run/xhim-release/candidate.env.release

test ! -e "$XHIM_CONFIG_SET"
test ! -e "$XHIM_CURRENT_NEXT"
umask 077
install -d -m 0700 "$XHIM_CONFIG_SET"

# server-only 必须复制当前文件且内容不变;server-and-config 才使用经过
# Preflight 的候选 Runtime Env 和候选 Compose。
install -m 0600 "$XHIM_SHARED/current/.env" "$XHIM_CONFIG_SET/.env"
install -m 0600 \
  "$XHIM_SHARED/current/compose.yaml" \
  "$XHIM_CONFIG_SET/compose.yaml"
install -d -m 0700 "$XHIM_CONFIG_SET/deploy"
install -m 0600 \
  "$XHIM_SHARED/current/deploy/Caddyfile" \
  "$XHIM_CONFIG_SET/deploy/Caddyfile"
install -m 0600 \
  "$XHIM_CANDIDATE_RELEASE_ENV" \
  "$XHIM_CONFIG_SET/.env.release"

server-and-config 应把上面复制的三个文件替换为同一次 Preflight 已绑定的 候选内容;随后重新核对 .env HMAC、Compose SHA-256、.env.release SHA-256、Caddyfile SHA-256、Commit/Version/Image Digest 和文件权限。发布时 必须安装 Preflight 使用的原始 candidate.env.release,不能重新拼接一个 “看起来相同”的文件。 示意:

bash
install -m 0600 \
  "$XHIM_CANDIDATE_RUNTIME_ENV" \
  "$XHIM_CONFIG_SET/.env"
install -m 0600 \
  server/compose.yaml \
  "$XHIM_CONFIG_SET/compose.yaml"
install -d -m 0700 "$XHIM_CONFIG_SET/deploy"
install -m 0600 \
  server/deploy/Caddyfile \
  "$XHIM_CONFIG_SET/deploy/Caddyfile"

四个文件完整落盘并校验后,执行 sync -f "$XHIM_CONFIG_SET",创建指向新 Config Set 的相对 Symlink,并用 rename(2) 语义替换 shared/current。该过程必须持有 唯一部署锁;不支持原子 Symlink Replace 的编排器不得执行 server-and-config。切换完成后只更新 Server Service:

bash
ln -s "config-sets/$XHIM_RELEASE_ID" "$XHIM_CURRENT_NEXT"
mv -Tf "$XHIM_CURRENT_NEXT" "$XHIM_SHARED/current"

docker compose \
  --project-name 'REPLACE_WITH_OBSERVED_PROJECT' \
  --env-file "$XHIM_SHARED/current/.env" \
  --env-file "$XHIM_SHARED/current/.env.release" \
  --file "$XHIM_SHARED/current/compose.yaml" \
  up -d --no-deps server

禁止 docker compose down、删除 Volume 或在同一命令重建 PostgreSQL/Gateway。 up -d --no-deps server 本身不是“依赖未变化”的证明;发布后 Observation 还 必须对比 PostgreSQL/Gateway 容器与 Image、Compose Network 和全部 Mount 身份。

8. 发布后 Observation 与 Postflight

先重新执行 observe-runtime,输出 runtime-after.json。Development 会从自动 register → password-login 探针取得短期 Token,不需要另外提供 Token 文件:

bash
scripts/deploy/server_release_gate.py postflight \
  --mode rollout \
  --preflight-file "$XHIM_EVIDENCE_DIR/preflight.json" \
  --deployment-manifest "$XHIM_DEPLOYMENT_MANIFEST" \
  --fingerprint-key-file "$XHIM_FINGERPRINT_KEY" \
  --runtime-observation-file "$XHIM_EVIDENCE_DIR/runtime-after.json" \
  --output "$XHIM_EVIDENCE_DIR/postflight.json"

Test/Production 没有 Development 登录接口,必须由客户认证服务或受保护运维作业 签发一个最小权限、短期、可撤销的用户 Access Token,并以绝对路径 0600 Regular File 注入。文件只含 Token 本身,不带 Bearer 前缀;Postflight 不会 把 Token 写入证据:

bash
XHIM_WSS_PROBE_TOKEN=/run/secrets/xhim-wss-probe-token

scripts/deploy/server_release_gate.py postflight \
  --mode rollout \
  --preflight-file "$XHIM_EVIDENCE_DIR/preflight.json" \
  --deployment-manifest "$XHIM_DEPLOYMENT_MANIFEST" \
  --fingerprint-key-file "$XHIM_FINGERPRINT_KEY" \
  --runtime-observation-file "$XHIM_EVIDENCE_DIR/runtime-after.json" \
  --websocket-probe-token-file "$XHIM_WSS_PROBE_TOKEN" \
  --output "$XHIM_EVIDENCE_DIR/postflight.json"

Postflight 必须同时观察到:

  • .env.release Commit/Version;
  • Container 实际 OCI Revision/Version;
  • Image ID Content Digest;
  • Candidate Compose Hash;
  • 原 Project/Volume;
  • 三个 Service 的 Container/Image/Network/Mount 观测;
  • server-only 下不变的依赖容器/Image、Network 和 Mount;
  • 在线 xhim_schema_migrations 与已签名 Migration Readiness;
  • 相同持久 Secret HMAC;
  • /health/ready/v1/sdk/config 的 Version/Protocol;
  • 使用公开 WebSocket URL、真实 TLS 校验和 Bearer Token 获得 101,且 UpgradeSec-WebSocket-AcceptSec-WebSocket-Protocol: xhim.v1 响应正确。

只把 Preflight 中的 Expected Commit 抄进报告不算验证。证据中的 deployed_revision_verified=true 仅在上述 Runtime Observation 完整匹配后出现。

Manifest 的 development_phone_auth_required=true 时,Runtime Env 必须配置 Development Phone Auth,公开配置必须返回 development_phone_auth_enabled=true,注册登录探针必须自动执行且成功;把 能力关闭或返回 false 不是跳过测试,而是发布失败。Production 的 Manifest 必须为 false,服务端也不得公开该能力。

HTTP 与已认证 WSS Upgrade Postflight 通过仍不是完整的 IM 验收。发布记录还 必须附上:

  • 至少两个账号在目标端正式 SDK 制品上完成登录、Bootstrap/Sync、A 发 B 收、 ACK/服务端序号、Ping/Pong、Sync Hint、已读同步和重新连接后的补偿同步;
  • 本次销售/交付覆盖的每个目标平台均有对应 SDK 版本、设备/系统版本、时间和 结果;客户端与服务端证据必须绑定同一 Server Version/Commit。

审计脚本会真实完成“认证成功的 WebSocket Upgrade”,不再只验证 HTTP;但它 不会发送完整 IM Frame,也不会代替真机上的消息、同步和重连回归。完整 SDK 验收仍由发布人员或测试编排执行并上传证据。

9. 受控应用回滚

回滚不复用普通 Rollout 的 24 小时 Preflight Age。先用上一次成功发布的 Preflight/Postflight 和 PostgreSQL Compatibility 声明生成一小时授权:

bash
scripts/deploy/server_release_gate.py rollback-preflight \
  --repository-root "$PWD" \
  --fingerprint-key-file "$XHIM_FINGERPRINT_KEY" \
  --target-preflight-file "$XHIM_EVIDENCE_DIR/old-preflight.json" \
  --target-postflight-file "$XHIM_EVIDENCE_DIR/old-postflight.json" \
  --compatibility-file /secure/xhim/postgres-compatibility.json \
  --output "$XHIM_EVIDENCE_DIR/rollback-authorization.json"

通过后:

  1. server-only 回滚切回旧 Image Digest,但仍要求 Runtime Env/Compose 与 依赖容器/Image、Network、Mount 不变;
  2. server-and-config 必须从同一个加密备份集恢复完整旧 Config Set,并在 一个部署锁内原子切回旧 .env、旧 Compose、旧 deploy/Caddyfile 和旧 .env.release;禁止只恢复其中一项后启动 Server;
  3. 仍只执行 up -d --no-deps server
  4. 重新执行 observe-runtime
  5. 执行回滚 Postflight,并重新完成 HTTP、已认证 WSS Upgrade 与 SDK 验收。 下面是 Test/Production 示例,因此必须再次提供受保护 Token 文件; Development 才可以省略该参数并使用自动注册登录 Token:
bash
scripts/deploy/server_release_gate.py postflight \
  --mode rollback \
  --rollback-authorization-file \
  "$XHIM_EVIDENCE_DIR/rollback-authorization.json" \
  --preflight-file "$XHIM_EVIDENCE_DIR/old-preflight.json" \
  --deployment-manifest "$XHIM_DEPLOYMENT_MANIFEST" \
  --fingerprint-key-file "$XHIM_FINGERPRINT_KEY" \
  --runtime-observation-file "$XHIM_EVIDENCE_DIR/runtime-after-rollback.json" \
  --websocket-probe-token-file /run/secrets/xhim-wss-probe-token \
  --output "$XHIM_EVIDENCE_DIR/rollback-postflight.json"

回滚授权只允许旧应用镜像,不授权数据库恢复或密钥轮换。数据库保留已经执行的 前向 Schema;Compatibility Gate 失败时停止自动回滚,进入暂停写入与前向修复 流程。

10. 完成状态

  • 已部署且审计通过:人工/受控编排已实际发布,备份、两次 Observation、 Preflight、Postflight、WSS 和目标端真机 SDK 回归齐全;
  • 代码完成,服务端未部署:没有真实服务器发布或 Runtime Observation;
  • 发布失败并已回滚:Rollback Authorization 与 Rollback Postflight 通过;
  • 发布失败且回滚受阻:停止自动操作并升级事故响应。

不存在“后台本地改完即默认部署”的状态。

XHIM 客户端 SDK 与服务端文档