关闭生产 SSH 私钥认证流程

生产服务器认证统一使用 SSH 密码,移除私钥管理路由、自动部署和私钥认证流程。
This commit is contained in:
xuans 2026-10-02 02:47:13 +08:00
parent dbe4f60c7d
commit 48fc5eb395
4 changed files with 33 additions and 209 deletions

View File

@ -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 主机指纹不能为空")

View File

@ -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

View File

@ -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,

View File

@ -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 密钥登录")
}