mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 23:18:52 +00:00
feat: M25 post-deployment TLS verification + M26 Traefik/Caddy targets
M25: After deploying a certificate, the agent probes the live TLS
endpoint and compares SHA-256 fingerprints to verify the correct cert
is being served. Best-effort — failures don't block deployments.
New endpoints: POST /jobs/{id}/verify, GET /jobs/{id}/verification.
Migration 000008 adds verification columns to jobs table.
M26: Traefik target connector (file provider, auto-reload) and Caddy
target connector (dual-mode: admin API hot-reload or file-based).
Both wired into agent dispatch.
Also: restructured README to highlight supported integrations (issuers,
targets, notifiers) earlier, moved API/CLI/MCP sections lower. Updated
all docs (features, connectors, architecture, testing guide, why-certctl)
and fixed integration tests for 18-param RegisterHandlers signature.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,9 +75,11 @@ const (
|
||||
type TargetType string
|
||||
|
||||
const (
|
||||
TargetTypeNGINX TargetType = "NGINX"
|
||||
TargetTypeApache TargetType = "Apache"
|
||||
TargetTypeHAProxy TargetType = "HAProxy"
|
||||
TargetTypeF5 TargetType = "F5"
|
||||
TargetTypeIIS TargetType = "IIS"
|
||||
TargetTypeNGINX TargetType = "NGINX"
|
||||
TargetTypeApache TargetType = "Apache"
|
||||
TargetTypeHAProxy TargetType = "HAProxy"
|
||||
TargetTypeF5 TargetType = "F5"
|
||||
TargetTypeIIS TargetType = "IIS"
|
||||
TargetTypeTraefik TargetType = "Traefik"
|
||||
TargetTypeCaddy TargetType = "Caddy"
|
||||
)
|
||||
|
||||
+16
-12
@@ -7,18 +7,22 @@ import (
|
||||
|
||||
// Job represents a unit of work in the certificate control plane.
|
||||
type Job struct {
|
||||
ID string `json:"id"`
|
||||
Type JobType `json:"type"`
|
||||
CertificateID string `json:"certificate_id"`
|
||||
TargetID *string `json:"target_id,omitempty"`
|
||||
Status JobStatus `json:"status"`
|
||||
Attempts int `json:"attempts"`
|
||||
MaxAttempts int `json:"max_attempts"`
|
||||
LastError *string `json:"last_error,omitempty"`
|
||||
ScheduledAt time.Time `json:"scheduled_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ID string `json:"id"`
|
||||
Type JobType `json:"type"`
|
||||
CertificateID string `json:"certificate_id"`
|
||||
TargetID *string `json:"target_id,omitempty"`
|
||||
Status JobStatus `json:"status"`
|
||||
Attempts int `json:"attempts"`
|
||||
MaxAttempts int `json:"max_attempts"`
|
||||
LastError *string `json:"last_error,omitempty"`
|
||||
ScheduledAt time.Time `json:"scheduled_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
VerificationStatus VerificationStatus `json:"verification_status"`
|
||||
VerifiedAt *time.Time `json:"verified_at,omitempty"`
|
||||
VerificationError *string `json:"verification_error,omitempty"`
|
||||
VerificationFp *string `json:"verification_fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
// JobType represents the classification of work to be performed.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// VerificationStatus represents the status of certificate deployment verification.
|
||||
type VerificationStatus string
|
||||
|
||||
const (
|
||||
// VerificationPending: verification has not yet been performed.
|
||||
VerificationPending VerificationStatus = "pending"
|
||||
// VerificationSuccess: the live TLS endpoint serves the expected certificate.
|
||||
VerificationSuccess VerificationStatus = "success"
|
||||
// VerificationFailed: the live TLS endpoint does not serve the expected certificate.
|
||||
VerificationFailed VerificationStatus = "failed"
|
||||
// VerificationSkipped: verification was skipped (disabled or not applicable).
|
||||
VerificationSkipped VerificationStatus = "skipped"
|
||||
)
|
||||
|
||||
// VerificationResult represents the outcome of verifying a deployed certificate
|
||||
// against the live TLS endpoint it should be serving.
|
||||
type VerificationResult struct {
|
||||
// JobID is the ID of the deployment job being verified.
|
||||
JobID string `json:"job_id"`
|
||||
// TargetID is the ID of the deployment target.
|
||||
TargetID string `json:"target_id"`
|
||||
// ExpectedFingerprint is the SHA-256 fingerprint of the certificate that was deployed.
|
||||
ExpectedFingerprint string `json:"expected_fingerprint"`
|
||||
// ActualFingerprint is the SHA-256 fingerprint of the certificate currently being served
|
||||
// at the live TLS endpoint.
|
||||
ActualFingerprint string `json:"actual_fingerprint"`
|
||||
// Verified is true if expected and actual fingerprints match.
|
||||
Verified bool `json:"verified"`
|
||||
// VerifiedAt is the timestamp when verification was performed.
|
||||
VerifiedAt time.Time `json:"verified_at"`
|
||||
// Error is a non-empty error message if verification failed to complete.
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestVerificationStatus_Constants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status VerificationStatus
|
||||
expected string
|
||||
}{
|
||||
{"Pending", VerificationPending, "pending"},
|
||||
{"Success", VerificationSuccess, "success"},
|
||||
{"Failed", VerificationFailed, "failed"},
|
||||
{"Skipped", VerificationSkipped, "skipped"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if string(tt.status) != tt.expected {
|
||||
t.Errorf("expected %s, got %s", tt.expected, string(tt.status))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerificationResult_Marshaling(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
result := &VerificationResult{
|
||||
JobID: "j-test123",
|
||||
TargetID: "t-nginx1",
|
||||
ExpectedFingerprint: "abc123def456",
|
||||
ActualFingerprint: "abc123def456",
|
||||
Verified: true,
|
||||
VerifiedAt: now,
|
||||
Error: "",
|
||||
}
|
||||
|
||||
if result.JobID != "j-test123" {
|
||||
t.Errorf("JobID mismatch: got %s", result.JobID)
|
||||
}
|
||||
if !result.Verified {
|
||||
t.Error("expected Verified to be true")
|
||||
}
|
||||
if result.Error != "" {
|
||||
t.Errorf("expected no error, got %s", result.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerificationResult_WithError(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
result := &VerificationResult{
|
||||
JobID: "j-test456",
|
||||
TargetID: "t-apache1",
|
||||
ExpectedFingerprint: "aaa111bbb222",
|
||||
ActualFingerprint: "ccc333ddd444",
|
||||
Verified: false,
|
||||
VerifiedAt: now,
|
||||
Error: "connection timeout",
|
||||
}
|
||||
|
||||
if result.Verified {
|
||||
t.Error("expected Verified to be false")
|
||||
}
|
||||
if result.Error != "connection timeout" {
|
||||
t.Errorf("expected error message, got %s", result.Error)
|
||||
}
|
||||
if result.ExpectedFingerprint == result.ActualFingerprint {
|
||||
t.Error("expected fingerprints to differ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user