From 9af39b071e0e7dd4e7a7c62cb15cb5029ac341bc Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 30 Sep 2026 16:09:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DSSH=E5=85=AC=E9=92=A5?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/deployment/ssh.go | 22 +++++++++++++--------- backend/internal/deployment/ssh_test.go | 13 +++++++++++++ backend/internal/httpapi/server.go | 2 ++ doc/Scert整改任务执行清单.md | 13 +++++++++++++ doc/Scert整改合并优化清单.md | 13 +++++++++++++ 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/backend/internal/deployment/ssh.go b/backend/internal/deployment/ssh.go index 04f46e5..c602eb7 100644 --- a/backend/internal/deployment/ssh.go +++ b/backend/internal/deployment/ssh.go @@ -351,15 +351,7 @@ func (c *sshClientAdapter) InstallAuthorizedKey(ctx context.Context, publicKey s return fmt.Errorf("SSH 公钥格式无效:%w", err) } encoded := base64.StdEncoding.EncodeToString(ssh.MarshalAuthorizedKey(key)) - script := fmt.Sprintf(`set -eu -umask 077 -install -d -m 700 "$HOME/.ssh" -touch "$HOME/.ssh/authorized_keys" -chmod 600 "$HOME/.ssh/authorized_keys" -decoded="$(printf '%s' '%s' | base64 -d)" -if ! grep -Fqx -- "$decoded" "$HOME/.ssh/authorized_keys"; then - printf '%%s\\n' "$decoded" >> "$HOME/.ssh/authorized_keys" -fi`, "%s", encoded) + script := authorizedKeyInstallScript(encoded) result, err := c.Run(ctx, []string{"sh", "-c", script}) if err != nil { return fmt.Errorf("部署 SSH 公钥失败:%w", err) @@ -369,6 +361,18 @@ fi`, "%s", encoded) } return nil } + +func authorizedKeyInstallScript(encoded string) string { + return fmt.Sprintf(`set -eu +umask 077 +install -d -m 700 "$HOME/.ssh" +touch "$HOME/.ssh/authorized_keys" +chmod 600 "$HOME/.ssh/authorized_keys" +decoded="$(printf '%%s' '%s' | base64 -d)" +if ! grep -Fqx -- "$decoded" "$HOME/.ssh/authorized_keys"; then + printf '%%s\n' "$decoded" >> "$HOME/.ssh/authorized_keys" +fi`, encoded) +} func (c *sshClientAdapter) Run(ctx context.Context, command []string) (CommandResult, error) { if len(command) == 0 { return CommandResult{}, errors.New("远程命令不能为空") diff --git a/backend/internal/deployment/ssh_test.go b/backend/internal/deployment/ssh_test.go index 0a761eb..8151b9d 100644 --- a/backend/internal/deployment/ssh_test.go +++ b/backend/internal/deployment/ssh_test.go @@ -66,3 +66,16 @@ func TestJoinShellArgsQuotesArguments(t *testing.T) { t.Fatalf("远程命令参数未正确转义:%s", joined) } } + +func TestAuthorizedKeyInstallScriptUsesRealNewline(t *testing.T) { + script := authorizedKeyInstallScript("encoded-public-key") + if !strings.Contains(script, "printf '%s' 'encoded-public-key'") { + t.Fatalf("authorized key payload was not inserted safely: %s", script) + } + if !strings.Contains(script, "printf '%s\\n'") { + t.Fatalf("authorized key should be terminated by a real newline: %s", script) + } + if strings.Contains(script, "printf '%s\\\\n'") { + t.Fatalf("authorized key script still writes a literal backslash-n: %s", script) + } +} diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index 8208ba4..011a2bb 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -971,6 +971,7 @@ func (s *Server) provisionServerSSHKey(ctx context.Context, item deployment.Serv } publicKey, err := s.sshKeys.AuthorizedPublicKey(ctx, item.PrivateKeyRef) if err != nil { + s.logSSHProvisionFailure("authorized_public_key", item, err) item.SSHKeyStatus = deployment.SSHKeyStatusFailed item.SSHKeyError = "自动部署 SSH 公钥失败" return item, err @@ -992,6 +993,7 @@ func (s *Server) provisionServerSSHKey(ctx context.Context, item deployment.Serv return item, err } if err := passwordClient.Close(); err != nil { + s.logSSHProvisionFailure("password_close", item, err) item.SSHKeyStatus = deployment.SSHKeyStatusFailed item.SSHKeyError = "自动部署 SSH 公钥失败" return item, err diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index d4337fa..4975f63 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1153,3 +1153,16 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 - [~] P0-10 其他任务、跨数据库实机矩阵和统一复审继续进行中。 - [ ] 真实生产数据库、密码 SSH、远端部署、HTTPS/DNS、ACME、Linux/Windows 服务和最终生产门禁仍未完成。 - [!] 不新增 `[✓]`,不以本地结果代替生产验收。 + +## 本轮开发进度(2026-09-30,P0-03 SSH 公钥部署阻塞修复) + +总体验收继续保持 **P0-03:完成,待复审**,本轮仅完成 SSH 公钥部署实现修正与本地回归,不代表生产部署验收通过。 + +- [x] 已完成,等待复审:修正公钥安装脚本的 `fmt.Sprintf` 参数传递,确保实际编码公钥被写入远端。 +- [x] 已完成,等待复审:修正 `authorized_keys` 追加逻辑,使用真实换行,不再写入字面量 `\\n`。 +- [x] 已完成,等待复审:补充公钥读取失败、密码连接关闭失败的 SSH 阶段日志。 +- [x] 已完成:新增公钥安装脚本回归测试;`go test ./...`、`go test -race ./internal/deployment ./internal/httpapi ./internal/app`、`go vet ./...`、`go build ./...`、`git diff --check` 均通过。 +- [~] 进行中:需重新构建正式候选包并部署到实机,通过现有失败服务器记录复测密码连接、公钥写入、私钥回连和状态持久化。 +- [ ] 未开始:证书文件推送、Nginx 配置校验/重载、失败回滚、真实多服务器部署和最终生产门禁。 +- [!] 第二台真实服务器仍不可用,不以虚构数据替代多服务器生产验收。 +- [!] 总体验收不得升级:仍为 **P0-03 完成,待复审**,没有新增 `[✓]`。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index 1d1cdeb..263948d 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1801,3 +1801,16 @@ P0-11 生产验收门禁 - [~] P0-10 其他任务、跨数据库实机矩阵和统一复审继续进行中。 - [ ] 真实生产数据库、密码 SSH、远端部署、HTTPS/DNS、ACME、Linux/Windows 服务和最终生产门禁仍未完成。 - [!] 不新增 `[✓]`,不以本地结果代替生产验收。 + +## 本轮开发进度(2026-09-30,P0-03 SSH 公钥部署阻塞修复) + +总体验收继续保持 **P0-03:完成,待复审**,本轮仅完成 SSH 公钥部署实现修正与本地回归,不代表生产部署验收通过。 + +- [x] 已完成,等待复审:修正公钥安装脚本的 `fmt.Sprintf` 参数传递,确保实际编码公钥被写入远端。 +- [x] 已完成,等待复审:修正 `authorized_keys` 追加逻辑,使用真实换行,不再写入字面量 `\\n`。 +- [x] 已完成,等待复审:补充公钥读取失败、密码连接关闭失败的 SSH 阶段日志。 +- [x] 已完成:新增公钥安装脚本回归测试;`go test ./...`、`go test -race ./internal/deployment ./internal/httpapi ./internal/app`、`go vet ./...`、`go build ./...`、`git diff --check` 均通过。 +- [~] 进行中:需重新构建正式候选包并部署到实机,通过现有失败服务器记录复测密码连接、公钥写入、私钥回连和状态持久化。 +- [ ] 未开始:证书文件推送、Nginx 配置校验/重载、失败回滚、真实多服务器部署和最终生产门禁。 +- [!] 第二台真实服务器仍不可用,不以虚构数据替代多服务器生产验收。 +- [!] 总体验收不得升级:仍为 **P0-03 完成,待复审**,没有新增 `[✓]`。