继续完善按服务器管理SSH密钥
This commit is contained in:
parent
38c3e2a389
commit
8211ac4973
@ -154,7 +154,7 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string
|
||||
RetryInterval: time.Duration(cfg.ACME.RenewalRetryIntervalMins) * time.Minute,
|
||||
}, management)
|
||||
server = httpapi.New(httpapi.Dependencies{
|
||||
Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys,
|
||||
Config: cfg, ConfigPath: configPath, Logger: logger, Repository: repo, Servers: serverRepo, Users: userRepo, SSHKeys: sshKeys, DeploymentFactory: deployment.SSHFactory{Secrets: secretStore},
|
||||
SetupDNS: dnsVerification, Secrets: secretStore, Firewall: firewall.NewManager(), AliyunChecker: aliyunAuthChecker,
|
||||
IssueCertificate: workflow.IssueForHTTP,
|
||||
IssueSetupCertificate: func(ctx context.Context, request certificate.SetupCertificateRequest) (certificate.SavedResource, error) {
|
||||
|
||||
@ -119,7 +119,7 @@ func shouldSkipMySQLStatement(ctx context.Context, db *sql.DB, statement string)
|
||||
}
|
||||
|
||||
func migrationName(version int) string {
|
||||
switch version {`n`tcase 9:`n`t return "server_ssh_key_status"
|
||||
switch version {
|
||||
case 1:
|
||||
return "initial"
|
||||
case 2:
|
||||
@ -136,6 +136,8 @@ func migrationName(version int) string {
|
||||
return "server_password_auth"
|
||||
case 8:
|
||||
return "user_groups"
|
||||
case 9:
|
||||
return "server_ssh_key_status"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_status VARCHAR(32) NOT NULL DEFAULT 'not_configured';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_fingerprint VARCHAR(255) NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_deployed_at VARCHAR(64) NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_tested_at VARCHAR(64) NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_error TEXT NOT NULL;
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_status TEXT NOT NULL DEFAULT 'not_configured';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_fingerprint TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_deployed_at TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_tested_at TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_error TEXT NOT NULL DEFAULT '';
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_status TEXT NOT NULL DEFAULT 'not_configured';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_fingerprint TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_deployed_at TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_tested_at TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_error TEXT NOT NULL DEFAULT '';
|
||||
@ -49,6 +49,7 @@ type Dependencies struct {
|
||||
Servers repository.ServerRepository
|
||||
Users repository.UserRepository
|
||||
SSHKeys *sshkey.Service
|
||||
DeploymentFactory deployment.ClientFactory
|
||||
SetupDNS setupDNSVerifier
|
||||
Secrets setupSecretWriter
|
||||
Firewall firewall.Manager
|
||||
@ -79,6 +80,7 @@ type Server struct {
|
||||
servers repository.ServerRepository
|
||||
users repository.UserRepository
|
||||
sshKeys *sshkey.Service
|
||||
deploymentFactory deployment.ClientFactory
|
||||
setupDNS setupDNSVerifier
|
||||
secrets setupSecretWriter
|
||||
firewall firewall.Manager
|
||||
@ -138,6 +140,7 @@ func New(deps Dependencies) *Server {
|
||||
servers: deps.Servers,
|
||||
users: deps.Users,
|
||||
sshKeys: deps.SSHKeys,
|
||||
deploymentFactory: deps.DeploymentFactory,
|
||||
issueCertificate: deps.IssueCertificate,
|
||||
issueSetupCertificate: deps.IssueSetupCertificate,
|
||||
rollbackSetupCertificate: deps.RollbackSetupCertificate,
|
||||
@ -551,6 +554,20 @@ func (s *Server) createDomain(c *gin.Context) {
|
||||
s.writeError(c, http.StatusInternalServerError, "保存域名失败", err)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
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 }
|
||||
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)
|
||||
}
|
||||
|
||||
@ -750,6 +767,18 @@ func (s *Server) updateServer(c *gin.Context) {
|
||||
s.writeError(c, serverErrorStatus(err), "更新服务器失败", err)
|
||||
return
|
||||
}
|
||||
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)
|
||||
@ -783,11 +812,11 @@ func (s *Server) prepareServerCredential(ctx context.Context, item deployment.Se
|
||||
}
|
||||
if password == "" && existing != nil && existing.PasswordRef != "" {
|
||||
item.PasswordRef = existing.PasswordRef
|
||||
return item, cleanup, nil
|
||||
}
|
||||
if password == "" {
|
||||
if item.PasswordRef == "" && password == "" {
|
||||
return deployment.Server{}, cleanup, errors.New("SSH \u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a")
|
||||
}
|
||||
if item.PasswordRef == "" {
|
||||
item.PasswordRef = "secret://ssh/password/" + domain.NewID()
|
||||
if err := s.secrets.Put(ctx, item.PasswordRef, map[string]string{"kind": "ssh_password", "password": password}); err != nil {
|
||||
return deployment.Server{}, cleanup, fmt.Errorf("\u4fdd\u5b58 SSH \u5bc6\u7801\u5931\u8d25\uff1a%w", err)
|
||||
@ -795,9 +824,78 @@ func (s *Server) prepareServerCredential(ctx context.Context, item deployment.Se
|
||||
cleanup = func() {
|
||||
_ = s.secrets.Delete(context.Background(), item.PasswordRef)
|
||||
}
|
||||
}
|
||||
if item.PrivateKeyRef == "" && s.sshKeys != nil && s.deploymentFactory != nil {
|
||||
metadata, _, err := s.sshKeys.CreateGenerated(ctx, "服务器 "+item.Name+" SSH密钥")
|
||||
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 {
|
||||
item.SSHKeyStatus = deployment.SSHKeyStatusFailed
|
||||
item.SSHKeyError = "自动部署 SSH 公钥失败"
|
||||
return item, err
|
||||
}
|
||||
item.SSHKeyStatus = deployment.SSHKeyStatusPending
|
||||
item.SSHKeyError = ""
|
||||
passwordClient, err := s.deploymentFactory.Connect(ctx, item)
|
||||
if err != nil {
|
||||
item.SSHKeyStatus = deployment.SSHKeyStatusFailed
|
||||
item.SSHKeyError = "自动部署 SSH 公钥失败"
|
||||
return item, err
|
||||
}
|
||||
if err := passwordClient.InstallAuthorizedKey(ctx, publicKey); err != nil {
|
||||
_ = passwordClient.Close()
|
||||
item.SSHKeyStatus = deployment.SSHKeyStatusFailed
|
||||
item.SSHKeyError = "自动部署 SSH 公钥失败"
|
||||
return item, err
|
||||
}
|
||||
if err := passwordClient.Close(); err != nil {
|
||||
item.SSHKeyStatus = deployment.SSHKeyStatusFailed
|
||||
item.SSHKeyError = "自动部署 SSH 公钥失败"
|
||||
return item, err
|
||||
}
|
||||
privateItem := item
|
||||
privateItem.AuthMethod = deployment.SSHAuthPrivateKey
|
||||
privateClient, err := s.deploymentFactory.Connect(ctx, privateItem)
|
||||
if err != nil {
|
||||
item.SSHKeyStatus = deployment.SSHKeyStatusFailed
|
||||
item.SSHKeyError = "自动验证 SSH 密钥登录失败"
|
||||
return item, err
|
||||
}
|
||||
defer privateClient.Close()
|
||||
result, err := privateClient.Run(ctx, []string{"true"})
|
||||
if err != nil {
|
||||
item.SSHKeyStatus = deployment.SSHKeyStatusFailed
|
||||
item.SSHKeyError = "自动验证 SSH 密钥登录失败"
|
||||
return item, err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
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) deleteServer(c *gin.Context) {
|
||||
if s.servers == nil {
|
||||
s.writeError(c, http.StatusNotImplemented, "服务器管理接口尚未配置", errors.New("服务器仓储未配置"))
|
||||
|
||||
@ -58,9 +58,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, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), server.ID, server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PrivateKeyRef, server.PasswordRef,
|
||||
server.HostKeyFingerprint, 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, 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)
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存服务器失败:%w", err)
|
||||
}
|
||||
@ -84,8 +84,8 @@ func (r *SQLServerRepository) UpdateServer(ctx context.Context, server deploymen
|
||||
if updatedAt.IsZero() {
|
||||
updatedAt = time.Now().UTC()
|
||||
}
|
||||
result, err := r.db.ExecContext(ctx, r.query(`UPDATE servers SET 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 = ?, updated_at = ? WHERE id = ?`),
|
||||
server.Name, server.Address, server.Port, server.Username, server.AuthMethod, server.PrivateKeyRef, server.PasswordRef, server.HostKeyFingerprint, server.CertificatePath, server.PrivateKeyPath, server.CertificateChainPath, server.WebServer, string(validationCommand), string(command), updatedAt, server.ID)
|
||||
result, err := r.db.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 = ? WHERE 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.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新服务器失败:%w", err)
|
||||
}
|
||||
@ -235,8 +235,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, certificate_path, private_key_path, certificate_chain_path, web_server, validation_command_json, reload_command_json, created_at, updated_at 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.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 FROM servers`
|
||||
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 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 FROM servers`
|
||||
|
||||
type rowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
@ -248,7 +248,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.CertificatePath, &server.PrivateKeyPath, &server.CertificateChainPath, &webServer, &validationCommandJSON, &commandJSON, &server.CreatedAt, &server.UpdatedAt); err != nil {
|
||||
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); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return deployment.Server{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
@ -119,6 +119,28 @@ func (s *Service) CreateGenerated(ctx context.Context, name string) (Metadata, s
|
||||
}
|
||||
return metadata, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPublicKey))), nil
|
||||
}
|
||||
func (s *Service) AuthorizedPublicKey(ctx context.Context, reference string) (string, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return "", errors.New("SSH 私钥存储未初始化")
|
||||
}
|
||||
metadata, err := s.metadata(ctx, strings.TrimSpace(reference), time.Time{}, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
payload, err := s.store.Get(ctx, metadata.Reference)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取 SSH 私钥失败:%w", err)
|
||||
}
|
||||
signer, err := parsePrivateKey(payload["private_key"], payload["passphrase"])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
publicKey, err := ssh.NewPublicKey(signer.PublicKey())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("编码 SSH 公钥失败:%w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey))), nil
|
||||
}
|
||||
func (s *Service) List(ctx context.Context) ([]Metadata, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return nil, errors.New("SSH 私钥存储未初始化")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user