关闭生产 SSH 私钥认证流程
生产服务器认证统一使用 SSH 密码,移除私钥管理路由、自动部署和私钥认证流程。
This commit is contained in:
parent
dbe4f60c7d
commit
48fc5eb395
@ -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 主机指纹不能为空")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 密钥登录")
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user