mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 22:58:52 +00:00
fix: end-to-end certificate lifecycle bugs + integration test environment
Fixes 12 production bugs preventing the full issuance→deployment flow from working with ACME (Pebble/Let's Encrypt) and step-ca issuers: ACME connector (acme.go): - Save orderURI before WaitOrder overwrites it (Go crypto/acme bug) - Add CreateOrderCert fallback via WaitOrder+FetchCert - Remove defer-reset in ValidateConfig that caused nil pointer panic - Add Insecure TLS option for self-signed ACME servers (Pebble) step-ca connector (stepca.go, jwe.go): - Real JWE provisioner key loading + decryption (was using ephemeral keys) - Fix JWT audience (/1.0/sign), sha claim (key fingerprint), kid header - Custom root CA trust via RootCertPath config - Remove hardcoded 90-day validity default (let step-ca decide) NGINX target connector (nginx.go): - Use sh -c for validate/reload commands (shell interpretation) - Use filepath.Dir instead of fragile string slicing - Add private key file writing (agent-mode keys were never deployed) - Make chain_path write conditional Server/service layer: - TriggerRenewalWithActor now creates actual Job records (was no-op) - createDeploymentJobs falls back to DB query when cert.TargetIDs empty - ProcessPendingJobs skips agent-routed deployment jobs - Agent cert pickup path parsing: len(parts)<4 → len(parts)<3 - Health/ready/auth-info endpoints bypass auth middleware - Write timeout 15s→120s for ACME issuance - Cert fingerprint computed on CSR submission Integration test environment (deploy/test/): - 10-phase test script covering Local CA, ACME, step-ca, revocation, discovery, renewal, and API spot checks - Docker Compose with 7 containers (server, agent, postgres, nginx, pebble, challtestsrv, step-ca) on isolated network - TLS verification checks SAN (not just Subject CN) for modern CA compat Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+42
-13
@@ -636,23 +636,50 @@ func (s *RenewalService) CompleteAgentCSRRenewal(ctx context.Context, job *domai
|
||||
}
|
||||
|
||||
// createDeploymentJobs creates pending deployment jobs for each target associated with a cert.
|
||||
// If cert.TargetIDs is empty (common — the repository doesn't populate this field),
|
||||
// falls back to querying certificate_target_mappings via targetRepo.ListByCertificate.
|
||||
func (s *RenewalService) createDeploymentJobs(ctx context.Context, cert *domain.ManagedCertificate) {
|
||||
if len(cert.TargetIDs) == 0 {
|
||||
// Resolve targets: prefer in-memory TargetIDs, fall back to DB query
|
||||
type targetInfo struct {
|
||||
id string
|
||||
agentID string
|
||||
}
|
||||
var targets []targetInfo
|
||||
|
||||
if len(cert.TargetIDs) > 0 {
|
||||
// TargetIDs populated (e.g. from test or manual wiring)
|
||||
for _, tid := range cert.TargetIDs {
|
||||
ti := targetInfo{id: tid}
|
||||
if s.targetRepo != nil {
|
||||
if target, err := s.targetRepo.Get(ctx, tid); err == nil && target.AgentID != "" {
|
||||
ti.agentID = target.AgentID
|
||||
}
|
||||
}
|
||||
targets = append(targets, ti)
|
||||
}
|
||||
} else if s.targetRepo != nil {
|
||||
// TargetIDs empty — query certificate_target_mappings via repository
|
||||
dbTargets, err := s.targetRepo.ListByCertificate(ctx, cert.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to query targets for certificate", "cert_id", cert.ID, "error", err)
|
||||
return
|
||||
}
|
||||
for _, t := range dbTargets {
|
||||
targets = append(targets, targetInfo{id: t.ID, agentID: t.AgentID})
|
||||
}
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
slog.Debug("no targets found for certificate, skipping deployment", "cert_id", cert.ID)
|
||||
return
|
||||
}
|
||||
for _, targetID := range cert.TargetIDs {
|
||||
tid := targetID
|
||||
|
||||
// Resolve agent_id from target for job routing
|
||||
for _, t := range targets {
|
||||
tid := t.id
|
||||
var agentIDPtr *string
|
||||
if s.targetRepo != nil {
|
||||
target, err := s.targetRepo.Get(ctx, tid)
|
||||
if err != nil {
|
||||
slog.Warn("failed to resolve agent for deployment job", "target_id", tid, "error", err)
|
||||
} else if target.AgentID != "" {
|
||||
agentID := target.AgentID
|
||||
agentIDPtr = &agentID
|
||||
}
|
||||
if t.agentID != "" {
|
||||
aid := t.agentID
|
||||
agentIDPtr = &aid
|
||||
}
|
||||
|
||||
deployJob := &domain.Job{
|
||||
@@ -667,7 +694,9 @@ func (s *RenewalService) createDeploymentJobs(ctx context.Context, cert *domain.
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := s.jobRepo.Create(ctx, deployJob); err != nil {
|
||||
slog.Error("failed to create deployment job for target", "target_id", targetID, "error", err)
|
||||
slog.Error("failed to create deployment job for target", "target_id", tid, "cert_id", cert.ID, "error", err)
|
||||
} else {
|
||||
slog.Info("created deployment job", "job_id", deployJob.ID, "cert_id", cert.ID, "target_id", tid, "agent_id", t.agentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user