From 213f030b4d33352e95f38a0d871dabfaa5748295 Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 16 Sep 2026 20:38:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E9=A6=96=E6=AC=A1=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E4=B8=B4=E6=97=B6=E8=AF=81=E4=B9=A6=E6=8C=87?= =?UTF-8?q?=E7=BA=B9=E6=A0=B8=E5=AF=B9=E4=B8=8E=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/bootstrap/notice_test.go | 28 +++++++++++++++++++ backend/internal/bootstrap/server.go | 11 ++++++-- .../bootstrap/server_lifecycle_test.go | 3 ++ deploy/README.md | 2 +- doc/生产部署与运维说明.md | 8 +++++- doc/首次IP初始化开发记录.md | 5 ++-- 6 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 backend/internal/bootstrap/notice_test.go diff --git a/backend/internal/bootstrap/notice_test.go b/backend/internal/bootstrap/notice_test.go new file mode 100644 index 0000000..e2c10be --- /dev/null +++ b/backend/internal/bootstrap/notice_test.go @@ -0,0 +1,28 @@ +package bootstrap + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "strings" + "testing" +) + +func TestBootstrapNoticeMatchesTLSCertificateFingerprint(t *testing.T) { + material, err := NewTemporaryTLSMaterial("203.0.113.20") + if err != nil { + t.Fatal(err) + } + var output bytes.Buffer + writeBootstrapNotice(&output, 8080, "/run/scert/bootstrap-url", material) + digest := sha256.Sum256(material.Leaf.Raw) + want := strings.ToUpper(hex.EncodeToString(digest[:])) + if !strings.Contains(output.String(), "SHA-256 指纹:"+want) || !strings.Contains(output.String(), "/run/scert/bootstrap-url") { + t.Fatal("启动提示缺少实际 TLS 证书的 SHA-256 指纹或受保护地址路径") + } + for _, forbidden := range []string{"token=", "PRIVATE KEY", "CERTIFICATE-----"} { + if strings.Contains(output.String(), forbidden) { + t.Fatal("启动提示不得泄露令牌、私钥或完整证书材料") + } + } +} diff --git a/backend/internal/bootstrap/server.go b/backend/internal/bootstrap/server.go index ef7a3ac..6f64f83 100644 --- a/backend/internal/bootstrap/server.go +++ b/backend/internal/bootstrap/server.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "crypto/sha256" "crypto/tls" "errors" "fmt" @@ -92,12 +93,18 @@ func Run(ctx context.Context, options ServerOptions) (bool, error) { } }() if options.Output != nil { - // 一次性令牌只能写入受保护文件,不进入终端日志或 systemd journal。 - fmt.Fprintf(options.Output, "首次初始化 HTTPS 服务已启动,端口 %d。请读取 %s 获取一次性访问地址;临时自签名证书需人工核对。\n", options.Config.Server.Port, options.URLPath) + writeBootstrapNotice(options.Output, options.Config.Server.Port, options.URLPath, material) } return serveTemporaryHTTPS(ctx, listener, material, handler, router.Completed()) } +func writeBootstrapNotice(output io.Writer, port int, path string, material TLSMaterial) { + // 指纹是公开证书摘要;一次性令牌和私钥仍只能留在受保护文件或内存中。 + fingerprint := sha256.Sum256(material.Certificate.Certificate[0]) + fmt.Fprintf(output, "首次初始化 HTTPS 服务已启动,端口 %d。请读取 %s 获取一次性访问地址。\n", port, path) + fmt.Fprintf(output, "Bootstrap 临时证书 SHA-256 指纹:%X\n请通过可信运维通道与浏览器证书详情核对;指纹不一致时不要输入初始化令牌、密码或 RAM 凭据。\n", fingerprint) +} + func serveTemporaryHTTPS(ctx context.Context, listener net.Listener, material TLSMaterial, handler http.Handler, completed <-chan struct{}) (bool, error) { requestContext, cancelRequests := context.WithCancel(context.Background()) defer cancelRequests() diff --git a/backend/internal/bootstrap/server_lifecycle_test.go b/backend/internal/bootstrap/server_lifecycle_test.go index 837e200..e13b71d 100644 --- a/backend/internal/bootstrap/server_lifecycle_test.go +++ b/backend/internal/bootstrap/server_lifecycle_test.go @@ -289,6 +289,9 @@ func TestBootstrapRunCancellationRemovesURLWithoutCreatingBusinessDatabase(t *te if parsed.Fragment == "" || strings.Contains(output.String(), parsed.Fragment) { t.Fatal("一次性令牌缺失或泄露到日志") } + if !strings.Contains(output.String(), "Bootstrap 临时证书 SHA-256 指纹:") { + t.Fatal("初始化启动日志缺少可通过可信运维通道核对的证书指纹") + } for _, path := range []string{fixture.urlPath, fixture.configPath, filepath.Join(fixture.base.Paths.Data, "scert.db")} { if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { t.Fatalf("取消后不应保留初始化地址或提前产生正式文件:%s", path) diff --git a/deploy/README.md b/deploy/README.md index e807d43..22789ed 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -76,7 +76,7 @@ sudo cat /run/scert/bootstrap-url 默认临时监听端口是 `8080`,须由管理员预先在主机防火墙和云安全组中对受信任的管理来源放行。云 NAT 环境应在启动前通过 systemd 的 `SCERT_BOOTSTRAP_PUBLIC_IP` 设置可访问的公网 IP;它只影响临时地址和证书,不会创建网络规则。 -在浏览器打开受保护文件内的完整 HTTPS 链接。链接片段是短期一次性令牌,不要存入普通文档、日志或截图。临时证书为自签名,不能作为公共 CA 信任或生产 TLS 通过的证据;当前版本的可信证书指纹核对流程仍待完善,只能先在受控管理网络验证。 +先打开裸 IP 地址查看浏览器证书详情,通过可信 SSH 或控制台读取最新 `journalctl -u Scert -b -n 30` 中的临时证书 SHA-256 指纹。与浏览器完全一致后,再打开受保护文件内的完整 HTTPS 链接。链接片段是短期一次性令牌,不要存入普通文档、日志或截图。指纹不一致必须停止,不得输入 RAM 凭据。临时证书为自签名,不能作为公共 CA 信任或生产 TLS 通过的证据;服务重启会生成新的临时证书,必须重新核对。 向导提供 SQLite、MySQL、PostgreSQL 三种存储选项,外部数据库强制 TLS 证书及主机名校验。数据库和阿里云 DNS 验证通过后可提交管理员、通知及正式域名/IP 配置;完成后临时服务关闭,自动加载正式配置并跳转正式 HTTPS 地址。IP 管理证书目前仍为自签名。 diff --git a/doc/生产部署与运维说明.md b/doc/生产部署与运维说明.md index 5345d29..6ec9d32 100644 --- a/doc/生产部署与运维说明.md +++ b/doc/生产部署与运维说明.md @@ -109,7 +109,13 @@ sudo cat /run/scert/bootstrap-url 通过可信运维通道读取并在浏览器打开完整 HTTPS 地址,不能只复制 IP 部分。链接片段包含短期一次性令牌;不要复制到日志、截图、工单或普通文档。地址文件仅允许服务用户和管理员读取,完成初始化或服务退出后会清理;令牌或会话过期时重启尚未初始化的服务再读取新地址,不要删除正式配置强制重新初始化。 -临时 HTTPS 使用自签名证书,不代表公共 CA 信任。当前版本尚缺少可由可信运维通道直接对照的证书指纹,首次证书身份核对流程仍是发布前待完善项;不得把“浏览器忽略证书警告”当作生产 TLS 验收。仅在受控管理网络完成开发验证。 +临时 HTTPS 使用自签名证书,不代表公共 CA 信任。浏览器首次打开裸 IP 地址时,应先查看证书详情,通过已核对 SSH 主机身份的运维连接或云控制台读取当前启动日志: + +```bash +sudo journalctl -u Scert -b -n 30 --no-pager +``` + +找到最新的 `Bootstrap 临时证书 SHA-256 指纹`,与浏览器证书详情的 SHA-256 指纹逐字比较(忽略显示分隔符和大小写)。必须一致才打开含一次性令牌的完整链接并填写敏感配置;不一致立即停止,不要输入 RAM 凭据或管理员密码。每次未初始化服务重启都会生成新证书,旧日志的指纹不能用于当前核对。指纹是公开证书摘要,日志不包含完整初始化链接、令牌或私钥。不得把“忽略证书警告”本身当作生产 TLS 验收;本轮仍缺少真实浏览器核对证据。 向导依次配置管理员、存储、阿里云 RAM 与域名验证、通知和正式管理入口: diff --git a/doc/首次IP初始化开发记录.md b/doc/首次IP初始化开发记录.md index b1876dc..8d8658a 100644 --- a/doc/首次IP初始化开发记录.md +++ b/doc/首次IP初始化开发记录.md @@ -33,6 +33,7 @@ - [✓] 桌面端已取得的处理器在配置保存后自动激活正式服务,无需重启窗口即可登录;并发切换、幂等关闭、取消在途初始化和损坏配置均有回归测试。 - [✓] 初始化写请求拒绝跨源访问;前端最终提交重新校验所有步骤和令牌有效期,避免响应乱序、计时缓存及重复提交绕过。 - [✓] 临时 HTTPS、SQLite 初始化、正式 HTTPS 同端口切换、管理员登录和会话可用性通过隔离集成测试。 +- [x] 临时 TLS 启动提示输出实际证书的 SHA-256 指纹供可信运维通道核对,令牌和私钥不进入日志;正在复审回归。 - [✓] 复审三库接入后的备份恢复兼容性:MySQL 接入加密凭据,PostgreSQL 未实现完整备份时明确拒绝,禁止漏备份却报告成功;内存凭据与拒绝操作的回归测试通过,不代表真实数据库备份恢复已验收。 - [!] PostgreSQL 完整备份恢复尚未实现,不能宣称三种数据库的全部运维能力均已完成。 - [!] 新版 Linux 发布包和桌面浏览器验收:本地独立服务启动及发布脚本执行此前被策略拒绝,未绕过限制。 @@ -92,7 +93,7 @@ PostgreSQL 当前可选择初始化,但原完整备份实现只有 SQLite/MySQ - 新启动链路已接入 `scert serve`,但独立服务的实际页面渲染和新版本 Linux 部署仍未验收。 - IP 管理入口目前使用自签名证书,不能表述为已获公共 CA 信任。 -- Bootstrap 临时证书仅保存在进程内;现有日志尚未提供可从可信运维通道对照的证书指纹,正式发布前应补齐首次证书身份核对流程。 +- Bootstrap 临时证书仅保存在进程内;启动日志提供实际证书 SHA-256 指纹,管理员须通过可信 SSH/控制台与浏览器核对,不一致则停止。每次未初始化服务重启都会生成新证书,不能复用旧指纹。 - 当前初始化仍调用防火墙管理器。systemd 使用非特权 `Scert` 用户且不授予网络管理能力;存在 UFW/firewalld 时必须验证只读检查和权限失败处理,不能要求把整个服务改成 root 来通过初始化。 - 本轮未连接远程主机,未操作真实 DNS,未清空既有数据库,未修改 `pcux.cn` 及其证书。 - `47.108.78.12` 用于项目部署;`8.137.23.95` 用于推送测试,两者不可混用。 @@ -100,4 +101,4 @@ PostgreSQL 当前可选择初始化,但原完整备份实现只有 SQLite/MySQ ## 下一步 -优先补齐 PostgreSQL 完整备份恢复、初始化证书身份核对及非特权防火墙处理;在允许启动服务和执行发布脚本的环境中完成浏览器与 Linux 发布验收。真实数据库和 DNS/ACME 测试必须使用专属测试数据与已授权域名,并明确测试范围,不能清空生产数据库或触碰禁止修改的域名。 +优先补齐 PostgreSQL 完整备份恢复及非特权防火墙处理;在允许启动服务和执行发布脚本的环境中完成浏览器、实际证书指纹核对与 Linux 发布验收。真实数据库和 DNS/ACME 测试必须使用专属测试数据与已授权域名,并明确测试范围,不能清空生产数据库或触碰禁止修改的域名。