完善多服务器逐台HTTPS校验
为证书部署流程增加按服务器地址连接、逐台验证和恢复校验能力,作为本轮继续开发前的检查点。
This commit is contained in:
parent
d06a001c64
commit
840d48d2e5
@ -21,6 +21,7 @@ type CertificateWorkflow struct {
|
||||
Notifier notification.Notifier
|
||||
NotifyError func(error)
|
||||
RemoteVerifier func(context.Context, string, []string) (certificate.ValidationResult, error)
|
||||
RemoteVerifierForServer func(context.Context, deployment.Server, string, []string) (certificate.ValidationResult, error)
|
||||
RemoteVerificationAttempts int
|
||||
RemoteVerificationInterval time.Duration
|
||||
RemoteVerificationWait func(context.Context, time.Duration) error
|
||||
@ -243,8 +244,9 @@ func (w *CertificateWorkflow) deploy(ctx context.Context, result certificate.Iss
|
||||
}
|
||||
targets = append(targets, deployment.Target{Server: resolved})
|
||||
}
|
||||
remoteVerificationEnabled := w.RemoteVerifier != nil || w.RemoteVerifierForServer != nil
|
||||
expectedFingerprint := ""
|
||||
if w.RemoteVerifier != nil {
|
||||
if remoteVerificationEnabled {
|
||||
// 从即将部署的文件读取身份,不信任可能缺失或陈旧的元数据;远端仍必须验证可信证书链。
|
||||
validation, validationErr := (certificate.LocalValidator{}).ValidateFiles(
|
||||
ctx, result.Saved.CertificatePath, result.Saved.FullChainPath, result.Saved.PrivateKeyPath, result.Domains,
|
||||
@ -259,25 +261,54 @@ func (w *CertificateWorkflow) deploy(ctx context.Context, result certificate.Iss
|
||||
FullChainPath: result.Saved.FullChainPath,
|
||||
PrivateKeyPath: result.Saved.PrivateKeyPath,
|
||||
}
|
||||
if w.RemoteVerifier != nil {
|
||||
if remoteVerificationEnabled {
|
||||
verificationDomain := verificationName(domainItem, result.Domains)
|
||||
if verificationDomain == "" {
|
||||
return deployment.Report{Status: "failed"}, errors.New("无法确定部署后的 HTTPS 校验域名,未执行推送")
|
||||
}
|
||||
endpoint := "https://" + verificationDomain
|
||||
// 首次部署允许不存在旧站点,但不得在缺少可信基线时宣称运行态已恢复。
|
||||
baseline, baselineErr := w.RemoteVerifier(ctx, endpoint, result.Domains)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return deployment.Report{Status: "failed"}, err
|
||||
type verificationTarget struct {
|
||||
server deployment.Server
|
||||
baselineFingerprint string
|
||||
}
|
||||
verificationTargets := make([]verificationTarget, 0, len(targets))
|
||||
allBaselinesAvailable := true
|
||||
for _, target := range targets {
|
||||
// 首次部署允许不存在旧站点,但不得在缺少可信基线时宣称运行态已恢复。
|
||||
baseline, baselineErr := w.verifyRemoteTarget(ctx, target.Server, endpoint, result.Domains)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return deployment.Report{Status: "failed"}, err
|
||||
}
|
||||
baselineFingerprint := ""
|
||||
if baselineErr == nil {
|
||||
baselineFingerprint = baseline.FingerprintSHA256
|
||||
}
|
||||
if baselineFingerprint == "" {
|
||||
allBaselinesAvailable = false
|
||||
}
|
||||
verificationTargets = append(verificationTargets, verificationTarget{
|
||||
server: target.Server,
|
||||
baselineFingerprint: baselineFingerprint,
|
||||
})
|
||||
}
|
||||
verification := deployment.Verification{
|
||||
Verify: func(verificationContext context.Context) error {
|
||||
return w.verifyRemoteDeployment(verificationContext, endpoint, result.Domains, expectedFingerprint)
|
||||
for _, target := range verificationTargets {
|
||||
if err := w.verifyRemoteDeploymentForServer(verificationContext, target.server, endpoint, result.Domains, expectedFingerprint); err != nil {
|
||||
return fmt.Errorf("服务器 %s 的 HTTPS 校验失败:%w", target.server.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if baselineErr == nil && baseline.FingerprintSHA256 != "" {
|
||||
if allBaselinesAvailable {
|
||||
verification.VerifyRecovery = func(recoveryContext context.Context) error {
|
||||
return w.verifyRemoteDeployment(recoveryContext, endpoint, result.Domains, baseline.FingerprintSHA256)
|
||||
for _, target := range verificationTargets {
|
||||
if err := w.verifyRemoteDeploymentForServer(recoveryContext, target.server, endpoint, result.Domains, target.baselineFingerprint); err != nil {
|
||||
return fmt.Errorf("服务器 %s 的恢复校验失败:%w", target.server.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return w.Deployment.DeploySavedVerified(ctx, targets, saved, verification)
|
||||
@ -292,7 +323,21 @@ func (w *CertificateWorkflow) deploy(ctx context.Context, result certificate.Iss
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (w *CertificateWorkflow) verifyRemoteTarget(ctx context.Context, server deployment.Server, endpoint string, expectedDomains []string) (certificate.ValidationResult, error) {
|
||||
if w.RemoteVerifierForServer != nil {
|
||||
return w.RemoteVerifierForServer(ctx, server, endpoint, expectedDomains)
|
||||
}
|
||||
if w.RemoteVerifier == nil {
|
||||
return certificate.ValidationResult{}, errors.New("远端 HTTPS 校验器未配置")
|
||||
}
|
||||
return w.RemoteVerifier(ctx, endpoint, expectedDomains)
|
||||
}
|
||||
|
||||
func (w *CertificateWorkflow) verifyRemoteDeployment(ctx context.Context, endpoint string, expectedDomains []string, expectedFingerprint string) error {
|
||||
return w.verifyRemoteDeploymentForServer(ctx, deployment.Server{}, endpoint, expectedDomains, expectedFingerprint)
|
||||
}
|
||||
|
||||
func (w *CertificateWorkflow) verifyRemoteDeploymentForServer(ctx context.Context, server deployment.Server, endpoint string, expectedDomains []string, expectedFingerprint string) error {
|
||||
attempts := w.RemoteVerificationAttempts
|
||||
if attempts <= 0 {
|
||||
attempts = defaultRemoteVerificationAttempts
|
||||
@ -311,7 +356,7 @@ func (w *CertificateWorkflow) verifyRemoteDeployment(ctx context.Context, endpoi
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
validation, err := w.RemoteVerifier(ctx, endpoint, expectedDomains)
|
||||
validation, err := w.verifyRemoteTarget(ctx, server, endpoint, expectedDomains)
|
||||
if err == nil {
|
||||
if contextErr := ctx.Err(); contextErr != nil {
|
||||
return contextErr
|
||||
|
||||
@ -119,6 +119,15 @@ type RemoteValidator struct {
|
||||
|
||||
// ValidateHTTPS 连接目标 HTTPS 地址并校验证书链和域名。
|
||||
func (v RemoteValidator) ValidateHTTPS(ctx context.Context, endpoint string, expectedDomains []string) (ValidationResult, error) {
|
||||
return v.validateHTTPS(ctx, endpoint, "", expectedDomains)
|
||||
}
|
||||
|
||||
// ValidateHTTPSAt 使用指定的远端地址建立 TCP 连接,同时保留 endpoint 的域名作为 SNI 和证书域名校验值。
|
||||
func (v RemoteValidator) ValidateHTTPSAt(ctx context.Context, endpoint, address string, expectedDomains []string) (ValidationResult, error) {
|
||||
return v.validateHTTPS(ctx, endpoint, strings.TrimSpace(address), expectedDomains)
|
||||
}
|
||||
|
||||
func (v RemoteValidator) validateHTTPS(ctx context.Context, endpoint, dialAddress string, expectedDomains []string) (ValidationResult, error) {
|
||||
target, err := normalizeHTTPSURL(endpoint)
|
||||
if err != nil {
|
||||
return ValidationResult{}, err
|
||||
@ -127,9 +136,13 @@ func (v RemoteValidator) ValidateHTTPS(ctx context.Context, endpoint string, exp
|
||||
if err != nil {
|
||||
return ValidationResult{}, fmt.Errorf("解析 HTTPS 地址失败:%w", err)
|
||||
}
|
||||
address := parsed.Host
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
address = net.JoinHostPort(parsed.Hostname(), "443")
|
||||
address := dialAddress
|
||||
if address == "" {
|
||||
address = parsed.Host
|
||||
}
|
||||
address, err = normalizeHTTPSDialAddress(address, parsed.Port())
|
||||
if err != nil {
|
||||
return ValidationResult{}, err
|
||||
}
|
||||
dialer := v.Dialer
|
||||
if dialer == nil {
|
||||
@ -173,6 +186,20 @@ func (v RemoteValidator) ValidateHTTPS(ctx context.Context, endpoint string, exp
|
||||
return validator.ValidatePEM(ctx, chainData, wanted)
|
||||
}
|
||||
|
||||
func normalizeHTTPSDialAddress(address, port string) (string, error) {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
return "", errors.New("HTTPS 连接地址不能为空")
|
||||
}
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(address); err == nil {
|
||||
return address, nil
|
||||
}
|
||||
return net.JoinHostPort(address, port), nil
|
||||
}
|
||||
|
||||
func (v RemoteValidator) timeout() time.Duration {
|
||||
if v.Timeout > 0 {
|
||||
return v.Timeout
|
||||
|
||||
Loading…
Reference in New Issue
Block a user