diff --git a/backend/internal/deployment/model.go b/backend/internal/deployment/model.go index 8b8c0cc..7bad27b 100644 --- a/backend/internal/deployment/model.go +++ b/backend/internal/deployment/model.go @@ -54,14 +54,16 @@ type Server struct { Port int `json:"port"` Username string `json:"username"` AuthMethod SSHAuthMethod `json:"auth_method"` - PrivateKeyRef string `json:"private_key_ref"` + // PrivateKeyRef 仅为历史数据库兼容保留,禁止进入 API 响应和生产认证流程。 + PrivateKeyRef string `json:"-"` PasswordRef string `json:"-"` PasswordConfigured bool `json:"password_configured"` - SSHKeyStatus SSHKeyStatus `json:"ssh_key_status"` - SSHKeyFingerprint string `json:"ssh_key_fingerprint,omitempty"` - SSHKeyDeployedAt string `json:"ssh_key_deployed_at,omitempty"` - SSHKeyTestedAt string `json:"ssh_key_tested_at,omitempty"` - SSHKeyError string `json:"ssh_key_error,omitempty"` + // 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"` @@ -88,12 +90,8 @@ func (s Server) Normalize() Server { s.SSHKeyDeployedAt = strings.TrimSpace(s.SSHKeyDeployedAt) s.SSHKeyTestedAt = strings.TrimSpace(s.SSHKeyTestedAt) s.SSHKeyError = strings.TrimSpace(s.SSHKeyError) - if s.AuthMethod == "" { - if s.PasswordRef != "" { - s.AuthMethod = SSHAuthPassword - } else { - s.AuthMethod = SSHAuthPrivateKey - } + if s.AuthMethod == "" || s.AuthMethod == SSHAuthPrivateKey { + s.AuthMethod = SSHAuthPassword } if s.SSHKeyStatus == "" { s.SSHKeyStatus = SSHKeyStatusNotConfigured @@ -214,17 +212,15 @@ func (s Server) Validate() error { if s.Username == "" { return errors.New("SSH 用户不能为空") } - switch s.AuthMethod { - case SSHAuthPrivateKey: - if s.PrivateKeyRef == "" { - return errors.New("SSH \u79c1\u94a5\u5f15\u7528\u4e0d\u80fd\u4e3a\u7a7a") - } - case SSHAuthPassword: - if s.PasswordRef == "" { - return errors.New("SSH \u5bc6\u7801\u5f15\u7528\u4e0d\u80fd\u4e3a\u7a7a") - } - default: - return fmt.Errorf("SSH \u8ba4\u8bc1\u65b9\u5f0f\u65e0\u6548\uff1a%s", s.AuthMethod) + // 历史内存模型仍允许读取旧私钥记录,实际 SSHFactory 会在连接前拒绝该认证方式。 + if s.PrivateKeyRef != "" && s.PasswordRef == "" { + return nil + } + if s.AuthMethod != SSHAuthPassword { + return errors.New("服务器仅支持 SSH 密码认证") + } + if s.PasswordRef == "" { + return errors.New("SSH 密码引用不能为空") } if s.HostKeyFingerprint == "" { return errors.New("SSH 主机指纹不能为空") diff --git a/backend/internal/deployment/ssh.go b/backend/internal/deployment/ssh.go index 16e057e..66634a9 100644 --- a/backend/internal/deployment/ssh.go +++ b/backend/internal/deployment/ssh.go @@ -45,38 +45,18 @@ func (f SSHFactory) connect(ctx context.Context, server Server, requireSFTP bool if f.Secrets == nil { return nil, errors.New("SSH 敏感信息存储未初始化") } - secretReference := server.PrivateKeyRef - if server.AuthMethod == SSHAuthPassword { - secretReference = server.PasswordRef + if server.AuthMethod != SSHAuthPassword || server.PrivateKeyRef != "" { + return nil, errors.New("服务器仅支持 SSH 密码认证,不能使用 SSH 私钥") } - secrets, err := f.Secrets.Get(ctx, secretReference) + secrets, err := f.Secrets.Get(ctx, server.PasswordRef) if err != nil { return nil, fmt.Errorf("读取 SSH 认证信息失败:%w", err) } - var authMethods []ssh.AuthMethod - if server.AuthMethod == SSHAuthPassword { - password := secrets["password"] - if password == "" { - return nil, errors.New("SSH 密码不能为空") - } - authMethods = []ssh.AuthMethod{ssh.Password(password)} - } else { - privateKey := strings.TrimSpace(secrets["private_key"]) - if privateKey == "" { - return nil, errors.New("SSH 私钥内容不能为空") - } - var signer ssh.Signer - passphrase := secrets["passphrase"] - if passphrase != "" { - signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase)) - } else { - signer, err = ssh.ParsePrivateKey([]byte(privateKey)) - } - if err != nil { - return nil, errors.New("解析 SSH 私钥失败:私钥格式或口令无效") - } - authMethods = []ssh.AuthMethod{ssh.PublicKeys(signer)} + password := secrets["password"] + if password == "" { + return nil, errors.New("SSH 密码不能为空") } + authMethods := []ssh.AuthMethod{ssh.Password(password)} timeout := f.Timeout if timeout <= 0 { timeout = 15 * time.Second diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index 778d6d5..879acb0 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -242,9 +242,6 @@ func (s *Server) registerRoutes() { api.POST("/servers", s.createServer) api.PUT("/servers/:id", s.updateServer) api.DELETE("/servers/:id", s.deleteServer) - adminAPI.GET("/ssh-keys", s.listSSHKeys) - adminAPI.POST("/ssh-keys", s.createSSHKey) - adminAPI.DELETE("/ssh-keys/:id", s.deleteSSHKey) s.router.NoRoute(s.frontend) } @@ -407,7 +404,6 @@ type serverRequest struct { Port int `json:"port"` Username string `json:"username" binding:"required"` AuthMethod string `json:"auth_method"` - PrivateKeyRef string `json:"private_key_ref"` Password string `json:"password"` HostKeyFingerprint string `json:"host_key_fingerprint" binding:"required"` CertificatePath string `json:"certificate_path"` @@ -828,18 +824,6 @@ func (s *Server) createServer(c *gin.Context) { return } s.recordServerHostKeyAudit(c, "server_host_key_fingerprint_register", item.ID) - item, provisionErr := s.provisionServerSSHKey(c.Request.Context(), item) - if provisionErr != nil { - if updateErr := s.servers.UpdateServer(c.Request.Context(), item); updateErr != nil && s.logger != nil { - s.logger.Warn("保存 SSH 密钥部署失败状态失败", "error", updateErr) - } - c.JSON(http.StatusBadGateway, gin.H{"error": item.SSHKeyError, "server": item}) - return - } - if err := s.servers.UpdateServer(c.Request.Context(), item); err != nil { - s.writeError(c, http.StatusInternalServerError, "保存 SSH 密钥部署状态失败", err) - return - } c.JSON(http.StatusCreated, item) } @@ -895,18 +879,6 @@ func (s *Server) updateServer(c *gin.Context) { if existing.HostKeyFingerprint != item.HostKeyFingerprint { s.recordServerHostKeyAudit(c, "server_host_key_fingerprint_update", item.ID) } - item, provisionErr := s.provisionServerSSHKey(c.Request.Context(), item) - if provisionErr != nil { - if updateErr := s.servers.UpdateServer(c.Request.Context(), item); updateErr != nil && s.logger != nil { - s.logger.Warn("保存 SSH 密钥部署失败状态失败", "error", updateErr) - } - c.JSON(http.StatusBadGateway, gin.H{"error": item.SSHKeyError, "server": item}) - return - } - if err := s.servers.UpdateServer(c.Request.Context(), item); err != nil { - s.writeError(c, http.StatusInternalServerError, "保存 SSH 密钥部署状态失败", err) - return - } if existing.PasswordRef != "" && existing.PasswordRef != item.PasswordRef && s.secrets != nil { if err := s.secrets.Delete(c.Request.Context(), existing.PasswordRef); err != nil && s.logger != nil { s.logger.Warn("删除旧 SSH 密码失败", "error", err) @@ -915,29 +887,13 @@ func (s *Server) updateServer(c *gin.Context) { c.JSON(http.StatusOK, item) } -func (s *Server) validateSSHKeyReference(ctx context.Context, reference, projectID string) error { - if s.sshKeys == nil { - return errors.New("SSH 私钥管理服务未配置") - } - _, err := s.sshKeys.ValidateReferenceForProject(ctx, reference, projectID) - return err -} - 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.AuthMethod == deployment.SSHAuthPrivateKey { - if err := s.validateSSHKeyReference(ctx, item.PrivateKeyRef, item.ProjectID); err != nil { - return deployment.Server{}, cleanup, err - } - if existing != nil && item.PasswordRef == "" { - item.PasswordRef = existing.PasswordRef - } - return item, cleanup, nil - } - if item.AuthMethod != deployment.SSHAuthPassword { - return deployment.Server{}, cleanup, errors.New("SSH \u8ba4\u8bc1\u65b9\u5f0f\u65e0\u6548") + 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") } @@ -956,97 +912,9 @@ func (s *Server) prepareServerCredential(ctx context.Context, item deployment.Se _ = s.secrets.Delete(context.Background(), item.PasswordRef) } } - if item.PrivateKeyRef == "" && s.sshKeys != nil && s.deploymentFactory != nil { - metadata, _, err := s.sshKeys.CreateGeneratedForProject(ctx, "服务器 "+item.Name+" SSH密钥", item.ProjectID) - if err != nil { - return deployment.Server{}, cleanup, fmt.Errorf("生成 SSH 密钥失败:%w", err) - } - item.PrivateKeyRef = metadata.Reference - item.SSHKeyFingerprint = metadata.Fingerprint - item.SSHKeyStatus = deployment.SSHKeyStatusPending - } return item, cleanup, nil } -func (s *Server) provisionServerSSHKey(ctx context.Context, item deployment.Server) (deployment.Server, error) { - item = item.Normalize() - if item.AuthMethod != deployment.SSHAuthPassword || item.PrivateKeyRef == "" || s.sshKeys == nil || s.deploymentFactory == nil { - return item, nil - } - 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 - } - item.SSHKeyStatus = deployment.SSHKeyStatusPending - item.SSHKeyError = "" - passwordClient, err := s.connectSSHForKey(ctx, item) - if err != nil { - s.logSSHProvisionFailure("password_connect", item, err) - item.SSHKeyStatus = deployment.SSHKeyStatusFailed - item.SSHKeyError = "自动部署 SSH 公钥失败" - return item, err - } - if err := passwordClient.InstallAuthorizedKey(ctx, publicKey); err != nil { - s.logSSHProvisionFailure("authorized_key_install", item, err) - _ = passwordClient.Close() - item.SSHKeyStatus = deployment.SSHKeyStatusFailed - item.SSHKeyError = "自动部署 SSH 公钥失败" - 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 - } - privateItem := item - privateItem.AuthMethod = deployment.SSHAuthPrivateKey - privateClient, err := s.connectSSHAfterReload(ctx, privateItem) - if err != nil { - s.logSSHProvisionFailure("private_key_connect", item, err) - item.SSHKeyStatus = deployment.SSHKeyStatusFailed - item.SSHKeyError = "自动验证 SSH 密钥登录失败" - return item, err - } - defer privateClient.Close() - result, err := privateClient.Run(ctx, []string{"true"}) - if err != nil { - s.logSSHProvisionFailure("private_key_command", item, err) - item.SSHKeyStatus = deployment.SSHKeyStatusFailed - item.SSHKeyError = "自动验证 SSH 密钥登录失败" - return item, err - } - if result.ExitCode != 0 { - s.logSSHProvisionFailure("private_key_exit", item, fmt.Errorf("远程退出码:%d", result.ExitCode)) - item.SSHKeyStatus = deployment.SSHKeyStatusFailed - item.SSHKeyError = "自动验证 SSH 密钥登录失败" - return item, fmt.Errorf("远程验证退出码:%d", result.ExitCode) - } - now := time.Now().UTC().Format(time.RFC3339) - item.AuthMethod = deployment.SSHAuthPrivateKey - item.SSHKeyStatus = deployment.SSHKeyStatusDeployed - item.SSHKeyDeployedAt = now - item.SSHKeyTestedAt = now - item.SSHKeyError = "" - return item, nil -} - -func (s *Server) connectSSHForKey(ctx context.Context, item deployment.Server) (deployment.Client, error) { - if factory, ok := s.deploymentFactory.(deployment.ShellClientFactory); ok { - return factory.ConnectShell(ctx, item) - } - return s.deploymentFactory.Connect(ctx, item) -} - -func (s *Server) logSSHProvisionFailure(stage string, item deployment.Server, err error) { - if s.logger == nil { - return - } - s.logger.Warn("服务器 SSH 密钥流程失败", "stage", stage, "server_id", item.ID, "server_name", item.Name, "error", err) -} func (s *Server) deleteServer(c *gin.Context) { if s.servers == nil { s.writeError(c, http.StatusNotImplemented, "服务器管理接口尚未配置", errors.New("服务器仓储未配置")) @@ -1114,7 +982,7 @@ func (s *Server) listServerDomains(c *gin.Context) { func serverFromRequest(request serverRequest) deployment.Server { return deployment.Server{ ID: domain.NewID(), Name: request.Name, Address: request.Address, Port: request.Port, - Username: request.Username, AuthMethod: deployment.SSHAuthMethod(request.AuthMethod), PrivateKeyRef: request.PrivateKeyRef, HostKeyFingerprint: request.HostKeyFingerprint, + Username: request.Username, AuthMethod: deployment.SSHAuthPassword, HostKeyFingerprint: request.HostKeyFingerprint, CertificatePath: request.CertificatePath, PrivateKeyPath: request.PrivateKeyPath, CertificateChainPath: request.CertificateChainPath, WebServer: deployment.WebServer(request.WebServer), ValidationCommand: request.ValidationCommand, ReloadCommand: request.ReloadCommand, diff --git a/backend/internal/httpapi/ssh_reload.go b/backend/internal/httpapi/ssh_reload.go index 3a2dd31..b688be2 100644 --- a/backend/internal/httpapi/ssh_reload.go +++ b/backend/internal/httpapi/ssh_reload.go @@ -3,32 +3,12 @@ package httpapi import ( "context" "errors" - "net" - "syscall" - "time" "git.pcux.cn/xuans/Scert/backend/internal/deployment" ) func (s *Server) connectSSHAfterReload(ctx context.Context, item deployment.Server) (deployment.Client, error) { - for attempt := 0; ; attempt++ { - if err := ctx.Err(); err != nil { - return nil, err - } - client, err := s.connectSSHForKey(ctx, item) - if err == nil { - return client, nil - } - var networkError *net.OpError - if attempt >= 4 || !errors.As(err, &networkError) || networkError.Op != "dial" || !errors.Is(err, syscall.ECONNREFUSED) { - return nil, err - } - timer := time.NewTimer((100 * time.Millisecond) << attempt) - select { - case <-ctx.Done(): - timer.Stop() - return nil, ctx.Err() - case <-timer.C: - } - } + _ = ctx + _ = item + return nil, errors.New("服务器仅支持 SSH 密码认证,已禁用 SSH 密钥登录") }