修复管理域名普通证书部署冲突
普通证书部署前核查受保护的 scert-nginx 管理入口策略,阻止覆盖管理域名并补充回归测试。
This commit is contained in:
parent
dc7bf25571
commit
cbd8c587d8
@ -242,7 +242,7 @@ func (w *CertificateWorkflow) deploy(ctx context.Context, result certificate.Iss
|
||||
if resolveErr != nil {
|
||||
return deployment.Report{}, fmt.Errorf("解析服务器 %s 的证书路径失败:%w", server.Name, resolveErr)
|
||||
}
|
||||
targets = append(targets, deployment.Target{Server: resolved})
|
||||
targets = append(targets, deployment.Target{Server: resolved, DomainName: domainItem.Name})
|
||||
}
|
||||
remoteVerificationEnabled := w.RemoteVerifier != nil || w.RemoteVerifierForServer != nil
|
||||
expectedFingerprint := ""
|
||||
|
||||
@ -3,41 +3,43 @@ package deployment
|
||||
type FailureCode string
|
||||
|
||||
const (
|
||||
FailureUnavailable FailureCode = "unavailable"
|
||||
FailureArtifact FailureCode = "artifact_invalid"
|
||||
FailureConfiguration FailureCode = "configuration_invalid"
|
||||
FailureConnection FailureCode = "connection_failed"
|
||||
FailureDirectory FailureCode = "directory_failed"
|
||||
FailureLock FailureCode = "lock_unavailable"
|
||||
FailureUpload FailureCode = "upload_failed"
|
||||
FailureReplace FailureCode = "replace_failed"
|
||||
FailureValidation FailureCode = "validation_failed"
|
||||
FailureRollback FailureCode = "rollback_failed"
|
||||
FailureRuntimeUnknown FailureCode = "runtime_unconfirmed"
|
||||
FailureCleanup FailureCode = "cleanup_failed"
|
||||
FailureLockRelease FailureCode = "lock_release_failed"
|
||||
FailureRecoveryPending FailureCode = "recovery_pending"
|
||||
FailureRecoveryUnknown FailureCode = "recovery_unconfirmed"
|
||||
FailureRecoveryRestored FailureCode = "recovery_restored"
|
||||
FailureUnavailable FailureCode = "unavailable"
|
||||
FailureArtifact FailureCode = "artifact_invalid"
|
||||
FailureConfiguration FailureCode = "configuration_invalid"
|
||||
FailureConnection FailureCode = "connection_failed"
|
||||
FailureDirectory FailureCode = "directory_failed"
|
||||
FailureLock FailureCode = "lock_unavailable"
|
||||
FailureUpload FailureCode = "upload_failed"
|
||||
FailureReplace FailureCode = "replace_failed"
|
||||
FailureValidation FailureCode = "validation_failed"
|
||||
FailureRollback FailureCode = "rollback_failed"
|
||||
FailureRuntimeUnknown FailureCode = "runtime_unconfirmed"
|
||||
FailureCleanup FailureCode = "cleanup_failed"
|
||||
FailureLockRelease FailureCode = "lock_release_failed"
|
||||
FailureRecoveryPending FailureCode = "recovery_pending"
|
||||
FailureRecoveryUnknown FailureCode = "recovery_unconfirmed"
|
||||
FailureRecoveryRestored FailureCode = "recovery_restored"
|
||||
FailureManagementConflict FailureCode = "management_domain_conflict"
|
||||
)
|
||||
|
||||
// 只公开固定诊断,不从远端输出、错误文本或文件路径推断可展示的消息。
|
||||
var publicFailureMessages = map[FailureCode]string{
|
||||
FailureRecoveryUnknown: "本次部署未完成,已恢复旧文件,但服务或站点恢复状态未确认,已保留部署锁;请管理员核查,勿直接重试或删除锁",
|
||||
FailureRecoveryRestored: "本次部署未完成,旧文件已恢复并通过配置校验及重载,站点已恢复部署前证书;请核查失败原因后再重试",
|
||||
FailureUnavailable: "部署服务不可用,请联系管理员检查服务配置",
|
||||
FailureArtifact: "待部署的证书或私钥校验失败,尚未推送到服务器",
|
||||
FailureConfiguration: "服务器部署配置无效,请检查认证、证书路径和命令配置",
|
||||
FailureConnection: "连接部署服务器失败,请检查网络、SSH 认证及主机身份配置",
|
||||
FailureDirectory: "无法创建证书目录,请检查部署账户的目录权限",
|
||||
FailureLock: "无法取得部署锁,可能有任务正在执行或存在中断遗留;请管理员核查远端文件、部署锁及服务状态后再重试,勿直接删除锁",
|
||||
FailureUpload: "证书上传失败,已尝试回滚;请检查受保护的服务日志后再重试",
|
||||
FailureReplace: "证书文件替换失败,已尝试回滚;请检查受保护的服务日志后再重试",
|
||||
FailureValidation: "服务配置校验失败,文件已回滚,未执行重载;请修正配置后再重试",
|
||||
FailureRollback: "部署失败且文件回滚未完成,已保留部署锁;请管理员核查远端文件及服务状态,勿直接重试或删除锁",
|
||||
FailureRuntimeUnknown: "服务重载失败,文件已回滚,但服务运行状态未确认;请管理员核对配置并恢复服务后再重试",
|
||||
FailureCleanup: "证书已推送并重载,但旧文件清理未完成;请管理员核查现场,不能据此确认站点新证书已生效",
|
||||
FailureLockRelease: "部署锁释放未完成;请管理员核查远端文件、部署锁及服务状态后再重试",
|
||||
FailureRecoveryUnknown: "本次部署未完成,已恢复旧文件,但服务或站点恢复状态未确认,已保留部署锁;请管理员核查,勿直接重试或删除锁",
|
||||
FailureRecoveryRestored: "本次部署未完成,旧文件已恢复并通过配置校验及重载,站点已恢复部署前证书;请核查失败原因后再重试",
|
||||
FailureUnavailable: "部署服务不可用,请联系管理员检查服务配置",
|
||||
FailureArtifact: "待部署的证书或私钥校验失败,尚未推送到服务器",
|
||||
FailureConfiguration: "服务器部署配置无效,请检查认证、证书路径和命令配置",
|
||||
FailureConnection: "连接部署服务器失败,请检查网络、SSH 认证及主机身份配置",
|
||||
FailureDirectory: "无法创建证书目录,请检查部署账户的目录权限",
|
||||
FailureLock: "无法取得部署锁,可能有任务正在执行或存在中断遗留;请管理员核查远端文件、部署锁及服务状态后再重试,勿直接删除锁",
|
||||
FailureUpload: "证书上传失败,已尝试回滚;请检查受保护的服务日志后再重试",
|
||||
FailureReplace: "证书文件替换失败,已尝试回滚;请检查受保护的服务日志后再重试",
|
||||
FailureValidation: "服务配置校验失败,文件已回滚,未执行重载;请修正配置后再重试",
|
||||
FailureRollback: "部署失败且文件回滚未完成,已保留部署锁;请管理员核查远端文件及服务状态,勿直接重试或删除锁",
|
||||
FailureRuntimeUnknown: "服务重载失败,文件已回滚,但服务运行状态未确认;请管理员核对配置并恢复服务后再重试",
|
||||
FailureCleanup: "证书已推送并重载,但旧文件清理未完成;请管理员核查现场,不能据此确认站点新证书已生效",
|
||||
FailureLockRelease: "部署锁释放未完成;请管理员核查远端文件、部署锁及服务状态后再重试",
|
||||
FailureManagementConflict: "该服务器的管理域名由受限激活服务接管,不能使用普通业务证书路径部署;请使用管理证书续期流程,或为业务域名配置独立 Nginx 站点",
|
||||
}
|
||||
|
||||
// PublicResult 保留结果事实,同时只允许固定错误码及中文诊断进入公开响应。
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@ -13,8 +15,9 @@ import (
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
Server Server
|
||||
Baseline RecoveryBaseline
|
||||
Server Server
|
||||
DomainName string
|
||||
Baseline RecoveryBaseline
|
||||
}
|
||||
|
||||
type Artifact struct {
|
||||
@ -95,7 +98,7 @@ func (s *Service) deploy(ctx context.Context, targets []Target, artifact Artifac
|
||||
return report
|
||||
}
|
||||
for index, target := range targets {
|
||||
result := s.deployOne(ctx, target.Server, artifact, pending, index, target.Baseline)
|
||||
result := s.deployOne(ctx, target.Server, target.DomainName, artifact, pending, index, target.Baseline)
|
||||
report.Results = append(report.Results, result)
|
||||
if result.Status == "success" {
|
||||
report.Success++
|
||||
@ -138,7 +141,7 @@ func loadArtifact(saved SavedFiles) (Artifact, error) {
|
||||
return Artifact{Certificate: certificateData, FullChain: fullChainData, PrivateKey: privateKeyData}, nil
|
||||
}
|
||||
|
||||
func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifact, pending *[]*pendingDeployment, index int, baseline RecoveryBaseline) (result Result) {
|
||||
func (s *Service) deployOne(ctx context.Context, server Server, domainName string, artifact Artifact, pending *[]*pendingDeployment, index int, baseline RecoveryBaseline) (result Result) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
server = server.Normalize()
|
||||
@ -159,6 +162,15 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
_ = client.Close()
|
||||
}
|
||||
}()
|
||||
if err := checkManagementDomainConflict(ctx, client, domainName); err != nil {
|
||||
if errors.Is(err, ErrManagementDomainConflict) {
|
||||
result.FailureCode = FailureManagementConflict
|
||||
} else {
|
||||
result.FailureCode = FailureConfiguration
|
||||
}
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
paths := []remoteFile{
|
||||
{path: RemotePath(server.CertificatePath), data: artifact.Certificate, mode: 0o644},
|
||||
{path: RemotePath(server.CertificateChainPath), data: artifact.FullChain, mode: 0o644},
|
||||
@ -577,6 +589,56 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
return result
|
||||
}
|
||||
|
||||
const managementPolicyPath = "/etc/scert-nginx/policy.json"
|
||||
|
||||
var ErrManagementDomainConflict = errors.New("该服务器的管理域名由受限激活服务接管,不能使用普通业务证书路径部署。请使用管理证书续期流程,或为业务域名配置独立 Nginx 站点")
|
||||
|
||||
type managementPolicyProbe struct {
|
||||
Version int `json:"version"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
func checkManagementDomainConflict(ctx context.Context, client Client, domainName string) error {
|
||||
domainName = normalizeDeploymentDomain(domainName)
|
||||
if domainName == "" {
|
||||
return nil
|
||||
}
|
||||
probe, err := client.Run(ctx, []string{"test", "-f", managementPolicyPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("核查服务器管理入口失败:%w", err)
|
||||
}
|
||||
if probe.ExitCode == 1 {
|
||||
return nil
|
||||
}
|
||||
if probe.ExitCode != 0 {
|
||||
return fmt.Errorf("核查服务器管理入口失败:命令退出码 %d", probe.ExitCode)
|
||||
}
|
||||
read, err := client.Run(ctx, []string{"cat", managementPolicyPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取服务器管理入口策略失败:%w", err)
|
||||
}
|
||||
if read.ExitCode != 0 {
|
||||
return fmt.Errorf("读取服务器管理入口策略失败:命令退出码 %d", read.ExitCode)
|
||||
}
|
||||
var policy managementPolicyProbe
|
||||
decoder := json.NewDecoder(bytes.NewReader(read.Output))
|
||||
if err := decoder.Decode(&policy); err != nil || policy.Version != 1 {
|
||||
return errors.New("服务器管理入口策略无效,已停止普通证书部署")
|
||||
}
|
||||
managementDomain := normalizeDeploymentDomain(policy.Hostname)
|
||||
if managementDomain == "" {
|
||||
return errors.New("服务器管理入口策略缺少管理域名,已停止普通证书部署")
|
||||
}
|
||||
if managementDomain == domainName {
|
||||
return ErrManagementDomainConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeDeploymentDomain(value string) string {
|
||||
return strings.ToLower(strings.TrimSuffix(strings.TrimSpace(value), "."))
|
||||
}
|
||||
|
||||
func (s *Service) newTransactionRecord(ctx context.Context, server Server, paths []remoteFile, lockPaths []string, id string, index int, baseline RecoveryBaseline) (*TransactionRecord, bool) {
|
||||
transactionContext, ok := TransactionContextFromContext(ctx)
|
||||
if !ok || s == nil || s.journal == nil {
|
||||
|
||||
@ -192,6 +192,33 @@ func TestDeployReplacesFilesAndCleansBackups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployRejectsProtectedManagementDomainBeforeChangingFiles(t *testing.T) {
|
||||
client := newMemoryClient()
|
||||
server := testServer("management-server")
|
||||
client.runResults = []CommandResult{
|
||||
{ExitCode: 0},
|
||||
{Output: []byte(`{"version":1,"hostname":"odacm.cn"}`), ExitCode: 0},
|
||||
}
|
||||
service := NewService(memoryFactory{clients: map[string]*memoryClient{server.ID: client}})
|
||||
before := map[string]memoryFile{}
|
||||
for path, file := range client.files {
|
||||
before[path] = file
|
||||
}
|
||||
report := service.Deploy(context.Background(), []Target{{Server: server, DomainName: "odacm.cn"}}, testArtifact(t))
|
||||
if report.Status != "failed" || report.Failed != 1 {
|
||||
t.Fatalf("管理域名冲突应拒绝部署:%+v", report)
|
||||
}
|
||||
if report.Results[0].FailureCode != FailureManagementConflict {
|
||||
t.Fatalf("失败码错误:%+v", report.Results[0])
|
||||
}
|
||||
if len(client.files) != len(before) {
|
||||
t.Fatalf("管理域名冲突前不应修改远端文件:before=%v after=%v", before, client.files)
|
||||
}
|
||||
if len(client.commands) != 2 || client.commands[0][0] != "test" || client.commands[1][0] != "cat" {
|
||||
t.Fatalf("管理域名冲突检查命令错误:%v", client.commands)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployReportsBackupCleanupFailure(t *testing.T) {
|
||||
client := newMemoryClient()
|
||||
client.removeError = ".scert-backup-"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user