# Phase 3 Multi-Credential Integration - Quick Reference **Full Plan:** [phase3_caddy_integration_completion.md](./phase3_caddy_integration_completion.md) ## 3-Step Implementation ### 1. Add Fields to DNSProviderConfig (manager.go:38-44) ```go type DNSProviderConfig struct { ID uint ProviderType string PropagationTimeout int Credentials map[string]string // Single-cred mode UseMultiCredentials bool // NEW ZoneCredentials map[string]map[string]string // NEW: map[baseDomain]credentials } ``` ### 2. Add Credential Resolution Loop (manager.go:~125) Insert after line 125 (after `dnsProviderConfigs` built): ```go // Phase 2: Resolve zone-specific credentials for multi-credential providers for i := range dnsProviderConfigs { cfg := &dnsProviderConfigs[i] // Find provider and check UseMultiCredentials flag var provider *models.DNSProvider for j := range dnsProviders { if dnsProviders[j].ID == cfg.ID { provider = &dnsProviders[j] break } } if provider == nil || !provider.UseMultiCredentials { continue // Skip single-credential providers } // Enable multi-credential mode cfg.UseMultiCredentials = true cfg.ZoneCredentials = make(map[string]map[string]string) // Preload credentials if err := m.db.Preload("Credentials").First(provider, provider.ID).Error; err != nil { logger.Log().WithError(err).WithField("provider_id", provider.ID).Warn("failed to preload credentials") continue } // Resolve credentials for each host's domain for _, host := range hosts { if !host.Enabled || host.DNSProviderID == nil || *host.DNSProviderID != provider.ID { continue } baseDomain := extractBaseDomain(host.DomainNames) if baseDomain == "" || cfg.ZoneCredentials[baseDomain] != nil { continue // Already resolved } // Resolve credential for this domain credentials, err := m.getCredentialForDomain(provider.ID, baseDomain, provider) if err != nil { logger.Log().WithError(err).WithField("domain", baseDomain).Warn("credential resolution failed") continue } cfg.ZoneCredentials[baseDomain] = credentials logger.Log().WithField("domain", baseDomain).Debug("resolved credential") } logger.Log().WithField("domains_resolved", len(cfg.ZoneCredentials)).Info("multi-credential resolution complete") } ``` ### 3. Update Config Generation (config.go:~190-198) Replace credential assembly logic in DNS challenge policy creation: ```go // Find DNS provider config dnsConfig, ok := dnsProviderMap[providerID] if !ok { logger.Log().WithField("provider_id", providerID).Warn("DNS provider not found") continue } // MULTI-CREDENTIAL MODE: Create separate policy per domain if dnsConfig.UseMultiCredentials && len(dnsConfig.ZoneCredentials) > 0 { for baseDomain, credentials := range dnsConfig.ZoneCredentials { // Find domains matching this base domain var matchingDomains []string for _, domain := range domains { if extractBaseDomain(domain) == baseDomain { matchingDomains = append(matchingDomains, domain) } } if len(matchingDomains) == 0 { continue } // Build provider config with zone-specific credentials providerConfig := map[string]any{"name": dnsConfig.ProviderType} for key, value := range credentials { providerConfig[key] = value } // Build issuer with DNS challenge (same as original, but with zone-specific credentials) var issuers []any // ... (same issuer creation logic as original, using providerConfig) // Create TLS automation policy for this domain tlsPolicies = append(tlsPolicies, &AutomationPolicy{ Subjects: dedupeDomains(matchingDomains), IssuersRaw: issuers, }) logger.Log().WithField("base_domain", baseDomain).Debug("created DNS challenge policy") } continue // Skip single-credential logic below } // SINGLE-CREDENTIAL MODE: Original logic (backward compatible) providerConfig := map[string]any{"name": dnsConfig.ProviderType} for key, value := range dnsConfig.Credentials { providerConfig[key] = value } // ... (rest of original logic) ``` ## Testing Checklist - [ ] Run `go test ./internal/caddy -run TestExtractBaseDomain` (should pass) - [ ] Run `go test ./internal/caddy -run TestMatchesZoneFilter` (should pass) - [ ] Run `go test ./internal/caddy -run TestManager_GetCredentialForDomain` (should pass) - [ ] Run `go test ./internal/caddy/...` (all tests should pass after changes) - [ ] Create integration test for multi-credential provider - [ ] Manual test: Create provider with 2+ credentials, verify separate TLS policies ## Validation Commands ```bash # Test helpers go test -v ./internal/caddy -run TestExtractBaseDomain go test -v ./internal/caddy -run TestMatchesZoneFilter # Test integration go test -v ./internal/caddy/... -count=1 # Check logs for credential resolution docker logs charon-app 2>&1 | grep "multi-credential" docker logs charon-app 2>&1 | grep "resolved credential" # Verify generated Caddy config curl -s http://localhost:2019/config/ | jq '.apps.tls.automation.policies[] | select(.subjects[] | contains("example"))' ``` ## Success Criteria ✅ All existing tests pass ✅ Helper function tests pass ✅ Integration tests pass (once added) ✅ Manual testing with 2+ domains works ✅ Backward compatibility validated ✅ Logs show credential selection ## Rollback If issues occur: 1. Set `UseMultiCredentials=false` on all providers via API 2. Restart Charon 3. Investigate logs for credential resolution errors ## Files Modified - `backend/internal/caddy/manager.go` - Add fields, add resolution loop - `backend/internal/caddy/config.go` - Update DNS challenge policy generation - `backend/internal/caddy/manager_multicred_integration_test.go` - Add integration tests (new file) ## Estimated Time - Implementation: 2-3 hours - Testing: 1-2 hours - Documentation: 1 hour - **Total: 4-6 hours**