feat(M34): dynamic issuer configuration with encrypted config storage

Replace static env-var-based issuer wiring with GUI-driven dynamic
configuration stored encrypted in PostgreSQL. Operators can now
configure, test, enable/disable, and manage issuers from the dashboard
without restarting the server.

Key changes:
- AES-256-GCM encryption for sensitive issuer config at rest (PBKDF2
  key derivation with 100k iterations)
- Dynamic IssuerRegistry with sync.RWMutex replacing static map
- Connector factory pattern (issuerfactory.NewFromConfig) replacing
  140 lines of static wiring in main.go
- Migration 000009: encrypted_config, last_tested_at, test_status,
  source columns on issuers table
- Env var seeding on first boot with ON CONFLICT DO NOTHING
- Registry Rebuild() for atomic map swap after CRUD operations
- Issuer type validation against domain constants on Create
- Audit trail for test connection results
- Conditional seeding for step-ca/OpenSSL (only when env vars set)
- GUI: source badge, connection test status on issuer detail page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-04-04 00:20:13 -04:00
parent 9954fd1100
commit 995b72df05
36 changed files with 1859 additions and 361 deletions
+13 -5
View File
@@ -43,9 +43,8 @@ func TestCertificateLifecycle(t *testing.T) {
localCA := local.New(nil, logger)
// Build issuer registry with adapter
issuerRegistry := map[string]service.IssuerConnector{
"iss-local": service.NewIssuerConnectorAdapter(localCA),
}
issuerRegistry := service.NewIssuerRegistry(logger)
issuerRegistry.Set("iss-local", service.NewIssuerConnectorAdapter(localCA))
// Initialize services (following dependency graph)
auditService := service.NewAuditService(auditRepo)
@@ -67,7 +66,7 @@ func TestCertificateLifecycle(t *testing.T) {
deploymentService := service.NewDeploymentService(jobRepo, targetRepo, agentRepo, certRepo, auditService, notificationService)
jobService := service.NewJobService(jobRepo, renewalService, deploymentService, logger)
agentService := service.NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry, renewalService)
issuerService := service.NewIssuerService(issuerRepo, auditService)
issuerService := service.NewIssuerService(issuerRepo, auditService, issuerRegistry, nil, slog.Default())
// Initialize handlers
certificateHandler := handler.NewCertificateHandler(certificateService)
@@ -90,7 +89,8 @@ func TestCertificateLifecycle(t *testing.T) {
verificationHandler := handler.NewVerificationHandler(&mockVerificationService{})
// EST handler — uses real Local CA issuer via ESTService
estService := service.NewESTService("iss-local", issuerRegistry["iss-local"], auditService, logger)
localCAConnector, _ := issuerRegistry.Get("iss-local")
estService := service.NewESTService("iss-local", localCAConnector, auditService, logger)
estHandler := handler.NewESTHandler(estService)
// Create router and register handlers
@@ -954,6 +954,14 @@ func (m *mockIssuerRepository) Update(ctx context.Context, issuer *domain.Issuer
return nil
}
func (m *mockIssuerRepository) CreateIfNotExists(ctx context.Context, issuer *domain.Issuer) (bool, error) {
if _, exists := m.issuers[issuer.ID]; exists {
return false, nil
}
m.issuers[issuer.ID] = issuer
return true, nil
}
func (m *mockIssuerRepository) Delete(ctx context.Context, id string) error {
delete(m.issuers, id)
return nil