From 5af752b25ac09e41803d14daee37a9329a4e513c Mon Sep 17 00:00:00 2001 From: xuans Date: Fri, 2 Oct 2026 05:03:31 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=B0=E5=BD=95SSH=E7=A7=81=E9=92=A5?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=95=B4=E6=94=B9=E4=B8=AD=E9=97=B4=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提交密码认证收敛、历史字段清理迁移及相关测试调整,后续继续修复测试并完成复审。 --- .../cli/sqlite_business_fixture_test.go | 14 +--- .../cli/sqlite_business_restore_test.go | 11 +-- .../internal/cli/sqlite_upgrade_cli_test.go | 2 +- backend/internal/database/migrate.go | 6 +- .../mysql/0019_password_auth_only.sql | 1 + .../postgres/0019_password_auth_only.sql | 1 + .../sqlite/0019_password_auth_only.sql | 1 + .../database/sqlite_business_upgrade_test.go | 14 +++- backend/internal/deployment/model.go | 73 +++++-------------- backend/internal/deployment/model_test.go | 4 +- backend/internal/deployment/ssh.go | 3 - backend/internal/deployment/ssh_test.go | 7 +- backend/internal/httpapi/server.go | 3 - backend/internal/httpapi/ssh_reload_test.go | 9 +-- .../internal/repository/server_repository.go | 29 ++------ .../repository/server_repository_test.go | 6 +- 16 files changed, 64 insertions(+), 120 deletions(-) create mode 100644 backend/internal/database/migrations/mysql/0019_password_auth_only.sql create mode 100644 backend/internal/database/migrations/postgres/0019_password_auth_only.sql create mode 100644 backend/internal/database/migrations/sqlite/0019_password_auth_only.sql diff --git a/backend/internal/cli/sqlite_business_fixture_test.go b/backend/internal/cli/sqlite_business_fixture_test.go index bbbfbc8..2a92099 100644 --- a/backend/internal/cli/sqlite_business_fixture_test.go +++ b/backend/internal/cli/sqlite_business_fixture_test.go @@ -20,7 +20,6 @@ import ( "git.pcux.cn/xuans/Scert/backend/internal/database" "git.pcux.cn/xuans/Scert/backend/internal/secretstore" "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/ssh" ) type sqliteBusinessFixture struct { @@ -54,17 +53,8 @@ func newSQLiteBusinessAssets(t *testing.T) sqliteBusinessFixture { if err := cfg.Save(fixture.path); err != nil { t.Fatal(err) } - sshKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - sshBlock, err := ssh.MarshalPrivateKey(sshKey, "隔离恢复测试") - if err != nil { - t.Fatal(err) - } fixture.secrets = map[string]map[string]string{ - cfg.DNS.CredentialRef: {"access_key_id": rand.Text(), "access_key_secret": rand.Text()}, - "secret://ssh/business": {"private_key": string(pem.EncodeToMemory(sshBlock))}, + cfg.DNS.CredentialRef: {"access_key_id": rand.Text(), "access_key_secret": rand.Text()}, } store, err := secretstore.Open(cfg.Paths.Config) if err != nil { @@ -103,7 +93,7 @@ func newSQLiteBusinessFixture(t *testing.T) sqliteBusinessFixture { {`INSERT INTO users (id, username, email, password_hash, role, groups_json) VALUES ('owner', 'recovery-owner', 'owner@example.test', ?, 'super_admin', '[]')`, []any{string(hash)}}, {`INSERT INTO users (id, username, email, password_hash, role, groups_json) VALUES ('member', 'recovery-member', 'member@example.test', ?, 'user', '["测试分组"]')`, []any{string(hash)}}, {`INSERT INTO domains (id, name, certificate_type, sans_json, status, expires_at, auto_renew, acme_environment, dns_provider) VALUES ('domain', 'example.test', 'single', '["example.test"]', 'issued', ?, 1, 'staging', 'alidns')`, []any{fixture.certificate.NotAfter}}, - {`INSERT INTO servers (id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json, validation_command_json) VALUES ('server', '恢复测试服务器', '192.0.2.10', 22, 'deploy', 'secret://ssh/business', 'test-only-fingerprint', '/test/cert.pem', '/test/key.pem', '/test/chain.pem', 'nginx', '["nginx","-s","reload"]', '["nginx","-t"]')`, nil}, + {`INSERT INTO servers (id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json, validation_command_json) VALUES ('server', '恢复测试服务器', '192.0.2.10', 22, 'deploy', '', 'test-only-fingerprint', '/test/cert.pem', '/test/key.pem', '/test/chain.pem', 'nginx', '["nginx","-s","reload"]', '["nginx","-t"]')`, nil}, {`INSERT INTO domain_servers (domain_id, server_id) VALUES ('domain', 'server')`, nil}, {`INSERT INTO tasks (id, domain_id, task_type, status, stage, message, operator_id, certificate_version_id, idempotency_key) VALUES ('task', 'domain', 'deploy', 'success', 'completed', '部署完成', 'owner', ?, 'recovery-idempotency')`, []any{fixture.certificate.Version}}, {`INSERT INTO task_attempts (id, task_id, attempt_number, stage, status, message) VALUES ('attempt', 'task', 1, 'deploy', 'success', '证书已推送')`, nil}, diff --git a/backend/internal/cli/sqlite_business_restore_test.go b/backend/internal/cli/sqlite_business_restore_test.go index 9fcf232..2a7c430 100644 --- a/backend/internal/cli/sqlite_business_restore_test.go +++ b/backend/internal/cli/sqlite_business_restore_test.go @@ -10,10 +10,10 @@ import ( "git.pcux.cn/xuans/Scert/backend/internal/config" "git.pcux.cn/xuans/Scert/backend/internal/database" + "git.pcux.cn/xuans/Scert/backend/internal/deployment" "git.pcux.cn/xuans/Scert/backend/internal/repository" "git.pcux.cn/xuans/Scert/backend/internal/secretstore" "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/ssh" ) func TestSQLiteBusinessBackupRestore(t *testing.T) { @@ -124,11 +124,6 @@ func assertSQLiteBusinessUsable(t *testing.T, fixture sqliteBusinessFixture) { if err != nil || !reflect.DeepEqual(actual, want) { t.Fatalf("恢复后的加密凭据无法正确读取:%v", err) } - if key := actual["private_key"]; key != "" { - if _, err := ssh.ParsePrivateKey([]byte(key)); err != nil { - t.Fatalf("恢复后的 SSH 私钥无法解析:%v", err) - } - } } if _, err := tls.LoadX509KeyPair(fixture.certificate.CertificatePath, fixture.certificate.PrivateKeyPath); err != nil { t.Fatalf("恢复后的证书与私钥不可用:%v", err) @@ -157,8 +152,8 @@ func assertSQLiteBusinessUsable(t *testing.T, fixture sqliteBusinessFixture) { } servers := repository.NewSQLServerRepository(db.DB) server, err := servers.GetServerByID(t.Context(), "server") - if err != nil || server.PrivateKeyRef != "secret://ssh/business" { - t.Fatalf("服务器凭据引用未恢复:%v", err) + if err != nil || server.AuthMethod != deployment.SSHAuthPassword || server.PasswordRef != "" { + t.Fatalf("服务器认证方式未按密码模式恢复:%v", err) } tasks := repository.NewSQLTaskRepository(db.DB) task, err := tasks.GetTask(t.Context(), "task") diff --git a/backend/internal/cli/sqlite_upgrade_cli_test.go b/backend/internal/cli/sqlite_upgrade_cli_test.go index 422c643..6fcba57 100644 --- a/backend/internal/cli/sqlite_upgrade_cli_test.go +++ b/backend/internal/cli/sqlite_upgrade_cli_test.go @@ -68,7 +68,7 @@ func TestSQLiteBusinessUpgradeStandaloneCLI(t *testing.T) { `INSERT INTO settings (key, value) VALUES ('historical', '历史设置')`, `INSERT INTO domains (id, name, certificate_type, sans_json, status, dns_provider) VALUES ('domain', 'example.test', 'single', '["example.test"]', 'issued', 'alidns')`, `INSERT INTO tasks (id, domain_id, task_type, status, message) VALUES ('task', 'domain', 'deploy', 'success', '历史任务')`, - `INSERT INTO servers (id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json) VALUES ('server', '历史服务器', '192.0.2.10', 22, 'deploy', 'secret://ssh/business', 'test-only', '/test/cert.pem', '/test/key.pem', '/test/chain.pem', 'nginx', '["nginx","-s","reload"]')`, + `INSERT INTO servers (id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json) VALUES ('server', '历史服务器', '192.0.2.10', 22, 'deploy', '', 'test-only', '/test/cert.pem', '/test/key.pem', '/test/chain.pem', 'nginx', '["nginx","-s","reload"]')`, `INSERT INTO domain_servers (domain_id, server_id) VALUES ('domain', 'server')`, `INSERT INTO audit_logs (action, resource_type, resource_id, status, message) VALUES ('deploy', 'domain', 'domain', 'success', '历史审计')`, } { diff --git a/backend/internal/database/migrate.go b/backend/internal/database/migrate.go index 797dd1e..1c1d00b 100644 --- a/backend/internal/database/migrate.go +++ b/backend/internal/database/migrate.go @@ -19,7 +19,7 @@ import ( //go:embed migrations/sqlite/0015_task_idempotency.sql migrations/mysql/0015_task_idempotency.sql migrations/postgres/0015_task_idempotency.sql //go:embed migrations/sqlite/0016_task_deployment_results.sql migrations/mysql/0016_task_deployment_results.sql migrations/postgres/0016_task_deployment_results.sql //go:embed migrations/sqlite/0017_deployment_transactions.sql migrations/mysql/0017_deployment_transactions.sql migrations/postgres/0017_deployment_transactions.sql -//go:embed migrations/sqlite/0018_recovery_evidence.sql migrations/mysql/0018_recovery_evidence.sql migrations/postgres/0018_recovery_evidence.sql +//go:embed migrations/sqlite/0018_recovery_evidence.sql migrations/mysql/0018_recovery_evidence.sql migrations/postgres/0018_recovery_evidence.sql migrations/sqlite/0019_password_auth_only.sql migrations/mysql/0019_password_auth_only.sql migrations/postgres/0019_password_auth_only.sql var migrationFiles embed.FS func Migrate(ctx context.Context, db *sql.DB, driver string) error { @@ -44,7 +44,7 @@ func Migrate(ctx context.Context, db *sql.DB, driver string) error { return nil } -const latestMigrationVersion = 18 +const latestMigrationVersion = 19 // currentMigrationVersion 在执行业务迁移前拒绝新版数据库及不连续的迁移历史。 func currentMigrationVersion(ctx context.Context, db *sql.DB) (int, error) { @@ -235,6 +235,8 @@ func migrationName(version int) string { return "deployment_transactions" case 18: return "recovery_evidence" + case 19: + return "password_auth_only" default: return "unknown" } diff --git a/backend/internal/database/migrations/mysql/0019_password_auth_only.sql b/backend/internal/database/migrations/mysql/0019_password_auth_only.sql new file mode 100644 index 0000000..287cf2f --- /dev/null +++ b/backend/internal/database/migrations/mysql/0019_password_auth_only.sql @@ -0,0 +1 @@ +UPDATE servers SET private_key_ref = '', auth_method = 'password', ssh_key_status = 'not_configured', ssh_key_fingerprint = '', ssh_key_deployed_at = '', ssh_key_tested_at = '', ssh_key_error = ''; \ No newline at end of file diff --git a/backend/internal/database/migrations/postgres/0019_password_auth_only.sql b/backend/internal/database/migrations/postgres/0019_password_auth_only.sql new file mode 100644 index 0000000..287cf2f --- /dev/null +++ b/backend/internal/database/migrations/postgres/0019_password_auth_only.sql @@ -0,0 +1 @@ +UPDATE servers SET private_key_ref = '', auth_method = 'password', ssh_key_status = 'not_configured', ssh_key_fingerprint = '', ssh_key_deployed_at = '', ssh_key_tested_at = '', ssh_key_error = ''; \ No newline at end of file diff --git a/backend/internal/database/migrations/sqlite/0019_password_auth_only.sql b/backend/internal/database/migrations/sqlite/0019_password_auth_only.sql new file mode 100644 index 0000000..287cf2f --- /dev/null +++ b/backend/internal/database/migrations/sqlite/0019_password_auth_only.sql @@ -0,0 +1 @@ +UPDATE servers SET private_key_ref = '', auth_method = 'password', ssh_key_status = 'not_configured', ssh_key_fingerprint = '', ssh_key_deployed_at = '', ssh_key_tested_at = '', ssh_key_error = ''; \ No newline at end of file diff --git a/backend/internal/database/sqlite_business_upgrade_test.go b/backend/internal/database/sqlite_business_upgrade_test.go index 28e8467..8874bd0 100644 --- a/backend/internal/database/sqlite_business_upgrade_test.go +++ b/backend/internal/database/sqlite_business_upgrade_test.go @@ -98,7 +98,7 @@ func createSQLiteHistoricalBusiness(t *testing.T, db *sql.DB, version int) { } if version >= 3 { statements = append(statements, - `INSERT INTO servers (id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json) VALUES ('server', '历史部署服务器', '192.0.2.10', 22, 'deploy', 'secret://ssh/historical', 'test-only', '/test/cert.pem', '/test/key.pem', '/test/chain.pem', 'nginx', '["nginx","-s","reload"]')`, + `INSERT INTO servers (id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json) VALUES ('server', '历史部署服务器', '192.0.2.10', 22, 'deploy', '', 'test-only', '/test/cert.pem', '/test/key.pem', '/test/chain.pem', 'nginx', '["nginx","-s","reload"]')`, `INSERT INTO domain_servers (domain_id, server_id) VALUES ('domain', 'server')`) } if version >= 4 { @@ -155,7 +155,17 @@ func snapshotSQLiteHistoricalBusiness(t *testing.T, db *sql.DB) map[string]sqlit if err := rows.Close(); err != nil { t.Fatal(err) } - for index, column := range columns { + if name == "servers" { + filtered := make([]string, 0, len(columns)) + for _, column := range columns { + switch column { + case "auth_method", "private_key_ref", "ssh_key_status", "ssh_key_fingerprint", "ssh_key_deployed_at", "ssh_key_tested_at", "ssh_key_error": + continue + } + filtered = append(filtered, column) + } + columns = filtered + } for index, column := range columns { columns[index] = `"` + strings.ReplaceAll(column, `"`, `""`) + `"` } query := "SELECT " + strings.Join(columns, ",") + ` FROM "` + name + `" ORDER BY 1` diff --git a/backend/internal/deployment/model.go b/backend/internal/deployment/model.go index 9ada3d2..dca34df 100644 --- a/backend/internal/deployment/model.go +++ b/backend/internal/deployment/model.go @@ -17,20 +17,9 @@ type WebServer string type SSHAuthMethod string const ( - SSHAuthPrivateKey SSHAuthMethod = "private_key" - SSHAuthPassword SSHAuthMethod = "password" + SSHAuthPassword SSHAuthMethod = "password" ) -const ( - SSHKeyStatusNotConfigured SSHKeyStatus = "not_configured" - SSHKeyStatusPending SSHKeyStatus = "pending" - SSHKeyStatusDeployed SSHKeyStatus = "deployed" - SSHKeyStatusManual SSHKeyStatus = "manual" - SSHKeyStatusFailed SSHKeyStatus = "failed" -) - -type SSHKeyStatus string - const ( WebServerNginx WebServer = "nginx" WebServerCaddy WebServer = "caddy" @@ -48,31 +37,23 @@ var serverPathPlaceholderPattern = regexp.MustCompile(`\{[^{}]+\}`) type Server struct { domain.ResourceScope - ID string `json:"id"` - Name string `json:"name"` - Address string `json:"address"` - Port int `json:"port"` - Username string `json:"username"` - AuthMethod SSHAuthMethod `json:"auth_method"` - // PrivateKeyRef 仅为历史数据库兼容保留,禁止进入 API 响应和生产认证流程。 - PrivateKeyRef string `json:"-"` - PasswordRef string `json:"-"` - PasswordConfigured bool `json:"password_configured"` - // SSH 密钥状态仅为历史数据兼容保留,不对外暴露。 - SSHKeyStatus SSHKeyStatus `json:"-"` - SSHKeyFingerprint string `json:"-"` - SSHKeyDeployedAt string `json:"-"` - SSHKeyTestedAt string `json:"-"` - SSHKeyError string `json:"-"` - HostKeyFingerprint string `json:"host_key_fingerprint"` - CertificatePath string `json:"certificate_path"` - PrivateKeyPath string `json:"private_key_path"` - CertificateChainPath string `json:"certificate_chain_path"` - WebServer WebServer `json:"web_server"` - ValidationCommand []string `json:"validation_command,omitempty"` - ReloadCommand []string `json:"reload_command,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + Port int `json:"port"` + Username string `json:"username"` + AuthMethod SSHAuthMethod `json:"auth_method"` + PasswordRef string `json:"-"` + PasswordConfigured bool `json:"password_configured"` + HostKeyFingerprint string `json:"host_key_fingerprint"` + CertificatePath string `json:"certificate_path"` + PrivateKeyPath string `json:"private_key_path"` + CertificateChainPath string `json:"certificate_chain_path"` + WebServer WebServer `json:"web_server"` + ValidationCommand []string `json:"validation_command,omitempty"` + ReloadCommand []string `json:"reload_command,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (s Server) Normalize() Server { @@ -82,23 +63,11 @@ func (s Server) Normalize() Server { s.Address = strings.TrimSpace(s.Address) s.Username = strings.TrimSpace(s.Username) s.AuthMethod = SSHAuthMethod(strings.ToLower(strings.TrimSpace(string(s.AuthMethod)))) - s.PrivateKeyRef = strings.TrimSpace(s.PrivateKeyRef) s.PasswordRef = strings.TrimSpace(s.PasswordRef) s.PasswordConfigured = s.PasswordRef != "" - s.SSHKeyStatus = SSHKeyStatus(strings.ToLower(strings.TrimSpace(string(s.SSHKeyStatus)))) - s.SSHKeyFingerprint = strings.TrimSpace(s.SSHKeyFingerprint) - s.SSHKeyDeployedAt = strings.TrimSpace(s.SSHKeyDeployedAt) - s.SSHKeyTestedAt = strings.TrimSpace(s.SSHKeyTestedAt) - s.SSHKeyError = strings.TrimSpace(s.SSHKeyError) - if s.AuthMethod == "" || s.AuthMethod == SSHAuthPrivateKey { + if s.AuthMethod == "" { s.AuthMethod = SSHAuthPassword } - if s.SSHKeyStatus == "" { - s.SSHKeyStatus = SSHKeyStatusNotConfigured - if s.PrivateKeyRef != "" { - s.SSHKeyStatus = SSHKeyStatusManual - } - } s.HostKeyFingerprint = strings.TrimSpace(s.HostKeyFingerprint) s.CertificatePath = strings.TrimSpace(s.CertificatePath) s.PrivateKeyPath = strings.TrimSpace(s.PrivateKeyPath) @@ -212,10 +181,6 @@ func (s Server) Validate() error { if s.Username == "" { return errors.New("SSH 用户不能为空") } - // 历史内存模型仍允许读取旧私钥记录,实际 SSHFactory 会在连接前拒绝该认证方式。 - if s.PrivateKeyRef != "" { - return errors.New("\u670d\u52a1\u5668\u4e0d\u5141\u8bb8\u4f7f\u7528 SSH \u79c1\u94a5\u8ba4\u8bc1") - } if s.AuthMethod != SSHAuthPassword { return errors.New("服务器仅支持 SSH 密码认证") } diff --git a/backend/internal/deployment/model_test.go b/backend/internal/deployment/model_test.go index 0ca1dd0..f7bebe5 100644 --- a/backend/internal/deployment/model_test.go +++ b/backend/internal/deployment/model_test.go @@ -121,9 +121,9 @@ func TestServerRejectsPasswordStyleOrUnsafeConfiguration(t *testing.T) { t.Fatal("missing SSH password reference should reject server configuration") } server = testServer("one") - server.PrivateKeyRef = "secret://ssh/private-key" + server.AuthMethod = SSHAuthMethod("unsupported") if err := server.Validate(); err == nil { - t.Fatal("SSH private key reference should reject server configuration") + t.Fatal("unsupported SSH authentication should reject server configuration") } server = testServer("one") server.ReloadCommand = []string{"nginx\nrm", "-s", "reload"} diff --git a/backend/internal/deployment/ssh.go b/backend/internal/deployment/ssh.go index f3cc45a..054a5ce 100644 --- a/backend/internal/deployment/ssh.go +++ b/backend/internal/deployment/ssh.go @@ -44,9 +44,6 @@ func (f SSHFactory) connect(ctx context.Context, server Server, requireSFTP bool if f.Secrets == nil { return nil, errors.New("SSH 敏感信息存储未初始化") } - if server.AuthMethod != SSHAuthPassword || server.PrivateKeyRef != "" { - return nil, errors.New("服务器仅支持 SSH 密码认证,不能使用 SSH 私钥") - } secrets, err := f.Secrets.Get(ctx, server.PasswordRef) if err != nil { return nil, fmt.Errorf("读取 SSH 认证信息失败:%w", err) diff --git a/backend/internal/deployment/ssh_test.go b/backend/internal/deployment/ssh_test.go index e2ccebb..de74241 100644 --- a/backend/internal/deployment/ssh_test.go +++ b/backend/internal/deployment/ssh_test.go @@ -13,19 +13,18 @@ func (s staticSecretReader) Get(_ context.Context, reference string) (map[string return s[reference], nil } -func TestSSHFactoryRejectsPrivateKeyAuthentication(t *testing.T) { +func TestSSHFactoryRejectsUnsupportedAuthentication(t *testing.T) { server := Server{ ID: "server-test", Name: "test server", Address: "127.0.0.1", Port: 1, - Username: "deploy", PrivateKeyRef: "secret://ssh/test", HostKeyFingerprint: "SHA256:test", + Username: "deploy", AuthMethod: SSHAuthMethod("unsupported"), HostKeyFingerprint: "SHA256:test", CertificatePath: "/tmp/cert.pem", PrivateKeyPath: "/tmp/key.pem", CertificateChainPath: "/tmp/fullchain.pem", WebServer: WebServerNginx, } factory := SSHFactory{ - Secrets: staticSecretReader{server.PrivateKeyRef: {"private_key": "must-not-be-read"}}, Timeout: 100 * time.Millisecond, } if _, err := factory.Connect(t.Context(), server); err == nil || !strings.Contains(err.Error(), "SSH") { - t.Fatalf("private key authentication should be rejected before reading secrets: %v", err) + t.Fatalf("unsupported authentication should be rejected before reading secrets: %v", err) } } diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index 5acea07..3791062 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -886,9 +886,6 @@ func (s *Server) updateServer(c *gin.Context) { func (s *Server) prepareServerCredential(ctx context.Context, item deployment.Server, password string, existing *deployment.Server) (deployment.Server, func(), error) { item = item.Normalize() cleanup := func() {} - if item.PrivateKeyRef != "" { - return deployment.Server{}, cleanup, errors.New("服务器仅支持 SSH 密码认证,不能使用 SSH 私钥") - } item.AuthMethod = deployment.SSHAuthPassword if s.secrets == nil { return deployment.Server{}, cleanup, errors.New("SSH \u5bc6\u7801\u5b58\u50a8\u672a\u521d\u59cb\u5316") diff --git a/backend/internal/httpapi/ssh_reload_test.go b/backend/internal/httpapi/ssh_reload_test.go index f1ae00e..6b5d2b7 100644 --- a/backend/internal/httpapi/ssh_reload_test.go +++ b/backend/internal/httpapi/ssh_reload_test.go @@ -16,17 +16,16 @@ func (f reloadTestFactory) Connect(ctx context.Context, item deployment.Server) return f.connect(ctx, item) } -func TestSSHAfterReloadRejectsPrivateKeyAuthentication(t *testing.T) { +func TestSSHAfterReloadRejectsUnsupportedAuthentication(t *testing.T) { server := &Server{deploymentFactory: reloadTestFactory{connect: func(context.Context, deployment.Server) (deployment.Client, error) { - t.Fatal("private key authentication must not reach the deployment factory") + t.Fatal("unsupported authentication must not reach the deployment factory") return nil, nil }}} client, err := server.connectSSHAfterReload(t.Context(), deployment.Server{ - AuthMethod: deployment.SSHAuthPrivateKey, - PrivateKeyRef: "secret://ssh/private-key", + AuthMethod: deployment.SSHAuthMethod("unsupported"), HostKeyFingerprint: "SHA256:registered", }) if client != nil || err == nil || !strings.Contains(err.Error(), "SSH") { - t.Fatalf("private key authentication should be rejected: client=%v err=%v", client, err) + t.Fatalf("unsupported authentication should be rejected: client=%v err=%v", client, err) } } diff --git a/backend/internal/repository/server_repository.go b/backend/internal/repository/server_repository.go index b1f3902..007d068 100644 --- a/backend/internal/repository/server_repository.go +++ b/backend/internal/repository/server_repository.go @@ -20,7 +20,6 @@ type ServerRepository interface { GetServerByID(ctx context.Context, id string) (deployment.Server, error) ListServers(ctx context.Context) ([]deployment.Server, error) DeleteServer(ctx context.Context, id string) error - IsPrivateKeyRefUsed(ctx context.Context, reference string) (bool, error) ListServersByDomainID(ctx context.Context, domainID string) ([]deployment.Server, error) ReplaceDomainServers(ctx context.Context, domainID string, serverIDs []string) error } @@ -60,9 +59,9 @@ func (r *SQLServerRepository) CreateServer(ctx context.Context, server deploymen updatedAt = createdAt } _, err = r.db.ExecContext(ctx, r.query(`INSERT INTO servers - (id, name, address, port, username, auth_method, private_key_ref, password_ref, host_key_fingerprint, ssh_key_status, ssh_key_fingerprint, ssh_key_deployed_at, ssh_key_tested_at, ssh_key_error, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at, project_id, owner_id, created_by, updated_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), server.ID, server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PrivateKeyRef, server.PasswordRef, - server.HostKeyFingerprint, server.SSHKeyStatus, server.SSHKeyFingerprint, server.SSHKeyDeployedAt, server.SSHKeyTestedAt, server.SSHKeyError, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), createdAt, updatedAt, + (id, name, address, port, username, auth_method, private_key_ref, password_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at, project_id, owner_id, created_by, updated_by) + VALUES (?, ?, ?, ?, ?, ?, '', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), server.ID, server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PasswordRef, + server.HostKeyFingerprint, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), createdAt, updatedAt, server.ProjectID, server.OwnerID, server.CreatedBy, server.UpdatedBy) if err != nil { return fmt.Errorf("保存服务器失败:%w", err) @@ -102,8 +101,8 @@ func (r *SQLServerRepository) UpdateServer(ctx context.Context, server deploymen if err := validateServerDomainPaths(ctx, tx, r.driver, server, nil); err != nil { return err } - _, err = tx.ExecContext(ctx, r.query(`UPDATE servers SET name = ?, address = ?, port = ?, username = ?, auth_method = ?, private_key_ref = ?, password_ref = ?, host_key_fingerprint = ?, ssh_key_status = ?, ssh_key_fingerprint = ?, ssh_key_deployed_at = ?, ssh_key_tested_at = ?, ssh_key_error = ?, certificate_path = ?, private_key_path = ?, certificate_chain_path = ?, web_server = ?, validation_command_json = ?, reload_command_json = ?, updated_at = ?, updated_by = ? WHERE id = ? AND project_id = ?`), - server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PrivateKeyRef, server.PasswordRef, server.HostKeyFingerprint, server.SSHKeyStatus, server.SSHKeyFingerprint, server.SSHKeyDeployedAt, server.SSHKeyTestedAt, server.SSHKeyError, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), updatedAt, server.UpdatedBy, server.ID, server.ProjectID) + _, err = tx.ExecContext(ctx, r.query(`UPDATE servers SET name = ?, address = ?, port = ?, username = ?, auth_method = ?, password_ref = ?, host_key_fingerprint = ?, certificate_path = ?, private_key_path = ?, certificate_chain_path = ?, web_server = ?, validation_command_json = ?, reload_command_json = ?, updated_at = ?, updated_by = ? WHERE id = ? AND project_id = ?`), + server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PasswordRef, server.HostKeyFingerprint, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), updatedAt, server.UpdatedBy, server.ID, server.ProjectID) if err != nil { return fmt.Errorf("更新服务器失败:%w", err) } @@ -168,18 +167,6 @@ func (r *SQLServerRepository) DeleteServer(ctx context.Context, id string) error return nil } -func (r *SQLServerRepository) IsPrivateKeyRefUsed(ctx context.Context, reference string) (bool, error) { - reference = strings.TrimSpace(reference) - if reference == "" { - return false, errors.New("SSH 私钥引用不能为空") - } - var count int - if err := r.db.QueryRowContext(ctx, r.query("SELECT COUNT(*) FROM servers WHERE private_key_ref = ?"), reference).Scan(&count); err != nil { - return false, fmt.Errorf("检查 SSH 私钥引用占用状态失败:%w", err) - } - return count > 0, nil -} - func (r *SQLServerRepository) ListServersByDomainID(ctx context.Context, domainID string) ([]deployment.Server, error) { rows, err := r.db.QueryContext(ctx, r.query(serverSelectQualified+` INNER JOIN domain_servers ds ON ds.server_id = servers.id WHERE ds.domain_id = ? ORDER BY servers.name, servers.id`), strings.TrimSpace(domainID)) if err != nil { @@ -254,8 +241,8 @@ func (r *SQLServerRepository) ReplaceDomainServers(ctx context.Context, domainID return nil } -const serverSelect = `SELECT id, name, address, port, username, auth_method, private_key_ref, password_ref, host_key_fingerprint, ssh_key_status, ssh_key_fingerprint, ssh_key_deployed_at, ssh_key_tested_at, ssh_key_error, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at, project_id, owner_id, created_by, updated_by FROM servers` -const serverSelectQualified = `SELECT servers.id, servers.name, servers.address, servers.port, servers.username, servers.auth_method, servers.private_key_ref, servers.password_ref, servers.host_key_fingerprint, servers.ssh_key_status, servers.ssh_key_fingerprint, servers.ssh_key_deployed_at, servers.ssh_key_tested_at, servers.ssh_key_error, servers.certificate_path, servers.private_key_path, servers.certificate_chain_path, servers.web_server, servers.validation_command_json, servers.reload_command_json, servers.created_at, servers.updated_at, servers.project_id, servers.owner_id, servers.created_by, servers.updated_by FROM servers` +const serverSelect = `SELECT id, name, address, port, username, auth_method, password_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at, project_id, owner_id, created_by, updated_by FROM servers` +const serverSelectQualified = `SELECT servers.id, servers.name, servers.address, servers.port, servers.username, servers.auth_method, servers.password_ref, servers.host_key_fingerprint, servers.certificate_path, servers.private_key_path, servers.certificate_chain_path, servers.web_server, servers.validation_command_json, servers.reload_command_json, servers.created_at, servers.updated_at, servers.project_id, servers.owner_id, servers.created_by, servers.updated_by FROM servers` type rowScanner interface { Scan(dest ...any) error @@ -267,7 +254,7 @@ func scanServer(row rowScanner) (deployment.Server, error) { var authMethod string var validationCommandJSON string var commandJSON string - if err := row.Scan(&server.ID, &server.Name, &server.Address, &server.Port, &server.Username, &authMethod, &server.PrivateKeyRef, &server.PasswordRef, &server.HostKeyFingerprint, &server.SSHKeyStatus, &server.SSHKeyFingerprint, &server.SSHKeyDeployedAt, &server.SSHKeyTestedAt, &server.SSHKeyError, &server.CertificatePath, &server.PrivateKeyPath, &server.CertificateChainPath, &webServer, &validationCommandJSON, &commandJSON, &server.CreatedAt, &server.UpdatedAt, &server.ProjectID, &server.OwnerID, &server.CreatedBy, &server.UpdatedBy); err != nil { + if err := row.Scan(&server.ID, &server.Name, &server.Address, &server.Port, &server.Username, &authMethod, &server.PasswordRef, &server.HostKeyFingerprint, &server.CertificatePath, &server.PrivateKeyPath, &server.CertificateChainPath, &webServer, &validationCommandJSON, &commandJSON, &server.CreatedAt, &server.UpdatedAt, &server.ProjectID, &server.OwnerID, &server.CreatedBy, &server.UpdatedBy); err != nil { if errors.Is(err, sql.ErrNoRows) { return deployment.Server{}, sql.ErrNoRows } diff --git a/backend/internal/repository/server_repository_test.go b/backend/internal/repository/server_repository_test.go index 80fe9a4..d3a53d4 100644 --- a/backend/internal/repository/server_repository_test.go +++ b/backend/internal/repository/server_repository_test.go @@ -100,15 +100,15 @@ func TestServerRepositoryRejectsInvalidServer(t *testing.T) { } } -func TestServerRepositoryRejectsPrivateKeyReference(t *testing.T) { +func TestServerRepositoryRejectsUnsupportedAuthentication(t *testing.T) { db := openServerTestDatabase(t) defer db.Close() repo := NewSQLServerRepository(db.DB) server := validTestServer() server.ID = "server-private-key-rejected" - server.PrivateKeyRef = "secret://ssh/private-key" + server.AuthMethod = deployment.SSHAuthMethod("unsupported") if err := repo.CreateServer(t.Context(), server); err == nil { - t.Fatal("server repository must reject SSH private key references") + t.Fatal("server repository must reject unsupported authentication") } }