增加初始化证书失败的脱敏诊断
This commit is contained in:
parent
b15e1be538
commit
d278cb436f
60
backend/internal/bootstrap/certificate_diagnostic.go
Normal file
60
backend/internal/bootstrap/certificate_diagnostic.go
Normal 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)
|
||||
}
|
||||
58
backend/internal/bootstrap/certificate_diagnostic_test.go
Normal file
58
backend/internal/bootstrap/certificate_diagnostic_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@ -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{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user