增加初始化证书失败的脱敏诊断

This commit is contained in:
xuans 2026-09-26 22:08:00 +08:00
parent b15e1be538
commit d278cb436f
3 changed files with 121 additions and 0 deletions

View File

@ -0,0 +1,60 @@
package bootstrap
import (
"context"
"errors"
"fmt"
"net"
"os"
"github.com/go-acme/lego/v5/acme"
)
// certificateFailureDiagnostic 只输出受控分类和数值,禁止输出远端说明、地址及凭据。
func certificateFailureDiagnostic(err error) string {
category := "unclassified"
status := 0
retrySeconds := int64(0)
var problem *acme.ProblemDetails
var rateLimit *acme.RateLimitedError
var network *net.OpError
switch {
case errors.As(err, &problem):
category = "acme_other"
if problem.HTTPStatus >= 100 && problem.HTTPStatus <= 599 {
status = problem.HTTPStatus
}
switch problem.Type {
case acme.RateLimitedErrorType:
category = "acme_rate_limited"
case acme.DNSErrorType:
category = "acme_dns"
case acme.ConnectionErrorType:
category = "acme_connection"
case acme.AccountDoesNotExistErrorType:
category = "acme_account_missing"
case acme.UnauthorizedErrorType:
category = "acme_unauthorized"
case acme.CaaErrorType:
category = "acme_caa"
case acme.InvalidContactErrorType:
category = "acme_invalid_contact"
case acme.RejectedIdentifierErrorType:
category = "acme_rejected_identifier"
}
if errors.As(err, &rateLimit) && rateLimit.RetryAfter > 0 {
retrySeconds = int64(rateLimit.RetryAfter.Seconds())
}
case errors.Is(err, context.DeadlineExceeded):
category = "deadline"
case errors.Is(err, context.Canceled):
category = "canceled"
case errors.Is(err, os.ErrPermission):
category = "filesystem_permission"
case errors.Is(err, os.ErrNotExist):
category = "filesystem_missing"
case errors.As(err, &network):
category = "network"
}
return fmt.Sprintf("管理证书签发失败:stage=management_certificate category=%s acme_status=%d retry_after_seconds=%d", category, status, retrySeconds)
}

View File

@ -0,0 +1,58 @@
package bootstrap
import (
"context"
"errors"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/go-acme/lego/v5/acme"
)
func TestCertificateDiagnosticRedactsUntrustedFields(t *testing.T) {
for _, item := range []struct {
name string
err error
category string
status int
retry int
}{
{"rate", &acme.RateLimitedError{ProblemDetails: &acme.ProblemDetails{
Type: acme.RateLimitedErrorType, HTTPStatus: 429, Detail: "secret-value",
URL: "https://secret-value/", Instance: "secret-value", Method: "secret-value",
SubProblems: []acme.SubProblem{{Type: "secret-value", Detail: "secret-value"}},
}, RetryAfter: 2 * time.Hour}, "acme_rate_limited", 429, 7200},
{"unknown", &acme.ProblemDetails{Type: "secret-value", Detail: "secret-value", HTTPStatus: -1}, "acme_other", 0, 0},
{"dns", &acme.ProblemDetails{Type: acme.DNSErrorType, HTTPStatus: 400}, "acme_dns", 400, 0},
{"permission", fmt.Errorf("secret-value: %w", os.ErrPermission), "filesystem_permission", 0, 0},
{"timeout", context.DeadlineExceeded, "deadline", 0, 0},
{"opaque", errors.New("secret-value"), "unclassified", 0, 0},
} {
t.Run(item.name, func(t *testing.T) {
message := certificateFailureDiagnostic(fmt.Errorf("secret-value: %w", item.err))
want := fmt.Sprintf("category=%s acme_status=%d retry_after_seconds=%d", item.category, item.status, item.retry)
if !strings.Contains(message, want) || strings.Contains(message, "secret-value") {
t.Fatalf("unsafe or incomplete diagnostic: %s", message)
}
})
}
}
func TestCompleterReportsSafeCertificateFailure(t *testing.T) {
fixture := newCompletionFixture(t, "admin.example.test")
fixture.issuer.err = &acme.ProblemDetails{Type: acme.RateLimitedErrorType, HTTPStatus: 429, Detail: "secret-value"}
var warnings []string
completer := fixture.completer(func(dependencies *CompleteDependencies) {
dependencies.Warn = func(message string) { warnings = append(warnings, message) }
})
_, err := completer.Complete(context.Background(), fixture.request("admin.example.test"))
if err == nil || len(warnings) != 1 || !strings.Contains(warnings[0], "acme_rate_limited") || strings.Contains(warnings[0], "secret-value") {
t.Fatalf("failure not safely reported: err=%v warnings=%v", err, warnings)
}
if _, err := os.Stat(fixture.configPath); !errors.Is(err, os.ErrNotExist) {
t.Fatal("failed certificate issuance must not commit configuration")
}
}

View File

@ -376,6 +376,9 @@ func (completer *Completer) issueManagementCertificate(ctx context.Context, cand
CertificateRoot: candidate.Paths.Certificate, BackupRoot: candidate.Paths.Backup,
})
if err != nil {
if completer.warn != nil {
completer.warn(certificateFailureDiagnostic(err))
}
return completionCertificate{}, completeError(CompleteErrorDependency, "申请管理域名证书失败", err)
}
return completionCertificate{