Simplify ACME onboarding hardening
- commit
- d015ae19fdb39852bf775d991955c6bdbd0f1a25
- author
- brice · · 2026-04-30 07:12 UTC
- committer
- brice · · 2026-04-30 07:17 UTC
- parent
- 345260e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +101 -127
M internal/app/run.go +42 -24
| @@ -622,7 +622,7 @@ func (si *serviceInit) initPublicTLS() error { | |||
| 622 | 622 | log.Info(). | |
| 623 | 623 | Str("email", si.cfg.TLS.ACME.Email). | |
| 624 | 624 | Str("challenge", string(effective.Mode)). | |
| 625 | - | Msg("public ACME TLS initialized (read-only, no renewal loop)") | |
| 625 | + | Msg("public ACME TLS initialized (runtime start deferred)") | |
| 626 | 626 | } | |
| 627 | 627 | ||
| 628 | 628 | si.svc.publicTLSSvc = svc | |
| @@ -630,6 +630,9 @@ func (si *serviceInit) initPublicTLS() error { | |||
| 630 | 630 | return nil | |
| 631 | 631 | } | |
| 632 | 632 | ||
| 633 | + | // publicTLSRuntime is the subset of in.PublicTLSService needed at server runtime | |
| 634 | + | // after the HTTP listener is bound: reconcile missing certs and start the renewal | |
| 635 | + | // loop. It is nil when ACME is disabled. | |
| 633 | 636 | type publicTLSRuntime interface { | |
| 634 | 637 | Reconcile(context.Context) error | |
| 635 | 638 | StartRenewalLoop(context.Context, time.Duration) <-chan struct{} | |
| @@ -2118,8 +2121,20 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||
| 2118 | 2121 | } | |
| 2119 | 2122 | ||
| 2120 | 2123 | httpsMux := http.NewServeMux() | |
| 2121 | - | if obHandler != nil { | |
| 2122 | - | httpsMux.Handle("/", gordonDomainOnboardingGate(cfg.Server.GordonDomain, obHandler, proxyHandler)) | |
| 2124 | + | if obHandler != nil && cfg.Server.GordonDomain != "" { | |
| 2125 | + | gordonDomain := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(cfg.Server.GordonDomain)), ".") | |
| 2126 | + | onboardingMux := http.NewServeMux() | |
| 2127 | + | registerOnboardingRoutes(onboardingMux, obHandler) | |
| 2128 | + | // Register onboarding paths host-gated so normal traffic hits | |
| 2129 | + | // proxyHandler directly through the catch-all / pattern. | |
| 2130 | + | httpsMux.Handle("GET /ca", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |
| 2131 | + | httpsMux.Handle("GET /ca.crt", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |
| 2132 | + | httpsMux.Handle("GET /ca.mobileconfig", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |
| 2133 | + | httpsMux.Handle("GET /.well-known/gordon/", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |
| 2134 | + | httpsMux.Handle("GET /.well-known/gordon/ca", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |
| 2135 | + | httpsMux.Handle("GET /.well-known/gordon/ca.crt", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |
| 2136 | + | httpsMux.Handle("GET /.well-known/gordon/ca.mobileconfig", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |
| 2137 | + | httpsMux.Handle("/", proxyHandler) | |
| 2123 | 2138 | } else { | |
| 2124 | 2139 | httpsMux.Handle("/", proxyHandler) | |
| 2125 | 2140 | } | |
| @@ -2140,6 +2155,19 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||
| 2140 | 2155 | return registryOut, proxyOut, httpsOut | |
| 2141 | 2156 | } | |
| 2142 | 2157 | ||
| 2158 | + | // registerOnboardingRoutes registers all CA onboarding HTTP routes on the | |
| 2159 | + | // given mux. Both direct-HTTP and Gordon-domain HTTPS onboarding use this. | |
| 2160 | + | func registerOnboardingRoutes(mux *http.ServeMux, ob *onboarding.Handler) { | |
| 2161 | + | mux.HandleFunc("GET /{$}", ob.ServeOnboardingPage) | |
| 2162 | + | mux.HandleFunc("GET /.well-known/gordon/", ob.ServeOnboardingPage) | |
| 2163 | + | mux.HandleFunc("GET /.well-known/gordon/ca", ob.ServeOnboardingPage) | |
| 2164 | + | mux.HandleFunc("GET /.well-known/gordon/ca.crt", ob.ServeCACert) | |
| 2165 | + | mux.HandleFunc("GET /.well-known/gordon/ca.mobileconfig", ob.ServeMobileconfig) | |
| 2166 | + | mux.HandleFunc("GET /ca", ob.ServeOnboardingPage) | |
| 2167 | + | mux.HandleFunc("GET /ca.crt", ob.ServeCACert) | |
| 2168 | + | mux.HandleFunc("GET /ca.mobileconfig", ob.ServeMobileconfig) | |
| 2169 | + | } | |
| 2170 | + | ||
| 2143 | 2171 | // directHTTPOnboardingGate returns an http.Handler that splits HTTP traffic | |
| 2144 | 2172 | // by source IP. Trusted proxy IPs flow through to the normal proxy chain. | |
| 2145 | 2173 | // Direct clients are served the CA onboarding flow on allowed paths and | |
| @@ -2148,14 +2176,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||
| 2148 | 2176 | func directHTTPOnboardingGate(ob *onboarding.Handler, proxyNets []*net.IPNet, proxyChain http.Handler, log zerowrap.Logger) http.Handler { | |
| 2149 | 2177 | // Build a small mux for direct-client onboarding paths. | |
| 2150 | 2178 | onboardingMux := http.NewServeMux() | |
| 2151 | - | onboardingMux.HandleFunc("GET /{$}", ob.ServeOnboardingPage) | |
| 2152 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/", ob.ServeOnboardingPage) | |
| 2153 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/ca", ob.ServeOnboardingPage) | |
| 2154 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/ca.crt", ob.ServeCACert) | |
| 2155 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/ca.mobileconfig", ob.ServeMobileconfig) | |
| 2156 | - | onboardingMux.HandleFunc("GET /ca", ob.ServeOnboardingPage) | |
| 2157 | - | onboardingMux.HandleFunc("GET /ca.crt", ob.ServeCACert) | |
| 2158 | - | onboardingMux.HandleFunc("GET /ca.mobileconfig", ob.ServeMobileconfig) | |
| 2179 | + | registerOnboardingRoutes(onboardingMux, ob) | |
| 2159 | 2180 | ||
| 2160 | 2181 | // Reserve ACME challenge path for future use. | |
| 2161 | 2182 | onboardingMux.HandleFunc("/.well-known/acme-challenge/", func(w http.ResponseWriter, _ *http.Request) { | |
| @@ -2182,7 +2203,9 @@ func directHTTPOnboardingGate(ob *onboarding.Handler, proxyNets []*net.IPNet, pr | |||
| 2182 | 2203 | }) | |
| 2183 | 2204 | } | |
| 2184 | 2205 | ||
| 2185 | - | func requestHostMatches(host, expected string) bool { | |
| 2206 | + | // canonicalHostsEqual compares two hosts after normalising both: stripping | |
| 2207 | + | // port, trimming spaces, lowercasing, and removing trailing dot. | |
| 2208 | + | func canonicalHostsEqual(host, expected string) bool { | |
| 2186 | 2209 | host = strings.TrimSpace(host) | |
| 2187 | 2210 | if h, _, err := net.SplitHostPort(host); err == nil { | |
| 2188 | 2211 | host = h | |
| @@ -2192,19 +2215,14 @@ func requestHostMatches(host, expected string) bool { | |||
| 2192 | 2215 | return host == expected | |
| 2193 | 2216 | } | |
| 2194 | 2217 | ||
| 2195 | - | func gordonDomainOnboardingGate(gordonDomain string, ob *onboarding.Handler, proxyHandler http.Handler) http.Handler { | |
| 2196 | - | mux := http.NewServeMux() | |
| 2197 | - | mux.HandleFunc("GET /ca", ob.ServeOnboardingPage) | |
| 2198 | - | mux.HandleFunc("GET /ca.crt", ob.ServeCACert) | |
| 2199 | - | mux.HandleFunc("GET /ca.mobileconfig", ob.ServeMobileconfig) | |
| 2200 | - | mux.HandleFunc("GET /.well-known/gordon/", ob.ServeOnboardingPage) | |
| 2201 | - | mux.HandleFunc("GET /.well-known/gordon/ca", ob.ServeOnboardingPage) | |
| 2202 | - | mux.HandleFunc("GET /.well-known/gordon/ca.crt", ob.ServeCACert) | |
| 2203 | - | mux.HandleFunc("GET /.well-known/gordon/ca.mobileconfig", ob.ServeMobileconfig) | |
| 2204 | - | ||
| 2218 | + | // gordonDomainOnboardingGate returns a handler that serves onboarding routes | |
| 2219 | + | // only when the request host matches gordonDomain. For mismatched hosts it | |
| 2220 | + | // delegates to proxyHandler. gordonDomain must already be canonicalised | |
| 2221 | + | // (trimmed, lowered, trailing dot removed). | |
| 2222 | + | func gordonDomainOnboardingGate(gordonDomain string, onboardingMux, proxyHandler http.Handler) http.Handler { | |
| 2205 | 2223 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 2206 | - | if gordonDomain != "" && requestHostMatches(r.Host, gordonDomain) { | |
| 2207 | - | mux.ServeHTTP(w, r) | |
| 2224 | + | if gordonDomain != "" && canonicalHostsEqual(r.Host, gordonDomain) { | |
| 2225 | + | onboardingMux.ServeHTTP(w, r) | |
| 2208 | 2226 | return | |
| 2209 | 2227 | } | |
| 2210 | 2228 | proxyHandler.ServeHTTP(w, r) | |
| @@ -622,7 +622,7 @@ func (si *serviceInit) initPublicTLS() error { | |||||
| 622 | log.Info(). | 622 | log.Info(). | ||
| 623 | Str("email", si.cfg.TLS.ACME.Email). | 623 | Str("email", si.cfg.TLS.ACME.Email). | ||
| 624 | Str("challenge", string(effective.Mode)). | 624 | Str("challenge", string(effective.Mode)). | ||
| 625 | - | Msg("public ACME TLS initialized (read-only, no renewal loop)") | 625 | + | Msg("public ACME TLS initialized (runtime start deferred)") |
| 626 | } | 626 | } | ||
| 627 | 627 | ||||
| 628 | si.svc.publicTLSSvc = svc | 628 | si.svc.publicTLSSvc = svc | ||
| @@ -630,6 +630,9 @@ func (si *serviceInit) initPublicTLS() error { | |||||
| 630 | return nil | 630 | return nil | ||
| 631 | } | 631 | } | ||
| 632 | 632 | ||||
| 633 | + | // publicTLSRuntime is the subset of in.PublicTLSService needed at server runtime | |||
| 634 | + | // after the HTTP listener is bound: reconcile missing certs and start the renewal | |||
| 635 | + | // loop. It is nil when ACME is disabled. | |||
| 633 | type publicTLSRuntime interface { | 636 | type publicTLSRuntime interface { | ||
| 634 | Reconcile(context.Context) error | 637 | Reconcile(context.Context) error | ||
| 635 | StartRenewalLoop(context.Context, time.Duration) <-chan struct{} | 638 | StartRenewalLoop(context.Context, time.Duration) <-chan struct{} | ||
| @@ -2118,8 +2121,20 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||||
| 2118 | } | 2121 | } | ||
| 2119 | 2122 | ||||
| 2120 | httpsMux := http.NewServeMux() | 2123 | httpsMux := http.NewServeMux() | ||
| 2121 | - | if obHandler != nil { | 2124 | + | if obHandler != nil && cfg.Server.GordonDomain != "" { |
| 2122 | - | httpsMux.Handle("/", gordonDomainOnboardingGate(cfg.Server.GordonDomain, obHandler, proxyHandler)) | 2125 | + | gordonDomain := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(cfg.Server.GordonDomain)), ".") |
| 2126 | + | onboardingMux := http.NewServeMux() | |||
| 2127 | + | registerOnboardingRoutes(onboardingMux, obHandler) | |||
| 2128 | + | // Register onboarding paths host-gated so normal traffic hits | |||
| 2129 | + | // proxyHandler directly through the catch-all / pattern. | |||
| 2130 | + | httpsMux.Handle("GET /ca", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |||
| 2131 | + | httpsMux.Handle("GET /ca.crt", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |||
| 2132 | + | httpsMux.Handle("GET /ca.mobileconfig", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |||
| 2133 | + | httpsMux.Handle("GET /.well-known/gordon/", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |||
| 2134 | + | httpsMux.Handle("GET /.well-known/gordon/ca", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |||
| 2135 | + | httpsMux.Handle("GET /.well-known/gordon/ca.crt", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |||
| 2136 | + | httpsMux.Handle("GET /.well-known/gordon/ca.mobileconfig", gordonDomainOnboardingGate(gordonDomain, onboardingMux, proxyHandler)) | |||
| 2137 | + | httpsMux.Handle("/", proxyHandler) | |||
| 2123 | } else { | 2138 | } else { | ||
| 2124 | httpsMux.Handle("/", proxyHandler) | 2139 | httpsMux.Handle("/", proxyHandler) | ||
| 2125 | } | 2140 | } | ||
| @@ -2140,6 +2155,19 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||||
| 2140 | return registryOut, proxyOut, httpsOut | 2155 | return registryOut, proxyOut, httpsOut | ||
| 2141 | } | 2156 | } | ||
| 2142 | 2157 | ||||
| 2158 | + | // registerOnboardingRoutes registers all CA onboarding HTTP routes on the | |||
| 2159 | + | // given mux. Both direct-HTTP and Gordon-domain HTTPS onboarding use this. | |||
| 2160 | + | func registerOnboardingRoutes(mux *http.ServeMux, ob *onboarding.Handler) { | |||
| 2161 | + | mux.HandleFunc("GET /{$}", ob.ServeOnboardingPage) | |||
| 2162 | + | mux.HandleFunc("GET /.well-known/gordon/", ob.ServeOnboardingPage) | |||
| 2163 | + | mux.HandleFunc("GET /.well-known/gordon/ca", ob.ServeOnboardingPage) | |||
| 2164 | + | mux.HandleFunc("GET /.well-known/gordon/ca.crt", ob.ServeCACert) | |||
| 2165 | + | mux.HandleFunc("GET /.well-known/gordon/ca.mobileconfig", ob.ServeMobileconfig) | |||
| 2166 | + | mux.HandleFunc("GET /ca", ob.ServeOnboardingPage) | |||
| 2167 | + | mux.HandleFunc("GET /ca.crt", ob.ServeCACert) | |||
| 2168 | + | mux.HandleFunc("GET /ca.mobileconfig", ob.ServeMobileconfig) | |||
| 2169 | + | } | |||
| 2170 | + | ||||
| 2143 | // directHTTPOnboardingGate returns an http.Handler that splits HTTP traffic | 2171 | // directHTTPOnboardingGate returns an http.Handler that splits HTTP traffic | ||
| 2144 | // by source IP. Trusted proxy IPs flow through to the normal proxy chain. | 2172 | // by source IP. Trusted proxy IPs flow through to the normal proxy chain. | ||
| 2145 | // Direct clients are served the CA onboarding flow on allowed paths and | 2173 | // Direct clients are served the CA onboarding flow on allowed paths and | ||
| @@ -2148,14 +2176,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||||
| 2148 | func directHTTPOnboardingGate(ob *onboarding.Handler, proxyNets []*net.IPNet, proxyChain http.Handler, log zerowrap.Logger) http.Handler { | 2176 | func directHTTPOnboardingGate(ob *onboarding.Handler, proxyNets []*net.IPNet, proxyChain http.Handler, log zerowrap.Logger) http.Handler { | ||
| 2149 | // Build a small mux for direct-client onboarding paths. | 2177 | // Build a small mux for direct-client onboarding paths. | ||
| 2150 | onboardingMux := http.NewServeMux() | 2178 | onboardingMux := http.NewServeMux() | ||
| 2151 | - | onboardingMux.HandleFunc("GET /{$}", ob.ServeOnboardingPage) | 2179 | + | registerOnboardingRoutes(onboardingMux, ob) |
| 2152 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/", ob.ServeOnboardingPage) | |||
| 2153 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/ca", ob.ServeOnboardingPage) | |||
| 2154 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/ca.crt", ob.ServeCACert) | |||
| 2155 | - | onboardingMux.HandleFunc("GET /.well-known/gordon/ca.mobileconfig", ob.ServeMobileconfig) | |||
| 2156 | - | onboardingMux.HandleFunc("GET /ca", ob.ServeOnboardingPage) | |||
| 2157 | - | onboardingMux.HandleFunc("GET /ca.crt", ob.ServeCACert) | |||
| 2158 | - | onboardingMux.HandleFunc("GET /ca.mobileconfig", ob.ServeMobileconfig) | |||
| 2159 | 2180 | ||||
| 2160 | // Reserve ACME challenge path for future use. | 2181 | // Reserve ACME challenge path for future use. | ||
| 2161 | onboardingMux.HandleFunc("/.well-known/acme-challenge/", func(w http.ResponseWriter, _ *http.Request) { | 2182 | onboardingMux.HandleFunc("/.well-known/acme-challenge/", func(w http.ResponseWriter, _ *http.Request) { | ||
| @@ -2182,7 +2203,9 @@ func directHTTPOnboardingGate(ob *onboarding.Handler, proxyNets []*net.IPNet, pr | |||||
| 2182 | }) | 2203 | }) | ||
| 2183 | } | 2204 | } | ||
| 2184 | 2205 | ||||
| 2185 | - | func requestHostMatches(host, expected string) bool { | 2206 | + | // canonicalHostsEqual compares two hosts after normalising both: stripping |
| 2207 | + | // port, trimming spaces, lowercasing, and removing trailing dot. | |||
| 2208 | + | func canonicalHostsEqual(host, expected string) bool { | |||
| 2186 | host = strings.TrimSpace(host) | 2209 | host = strings.TrimSpace(host) | ||
| 2187 | if h, _, err := net.SplitHostPort(host); err == nil { | 2210 | if h, _, err := net.SplitHostPort(host); err == nil { | ||
| 2188 | host = h | 2211 | host = h | ||
| @@ -2192,19 +2215,14 @@ func requestHostMatches(host, expected string) bool { | |||||
| 2192 | return host == expected | 2215 | return host == expected | ||
| 2193 | } | 2216 | } | ||
| 2194 | 2217 | ||||
| 2195 | - | func gordonDomainOnboardingGate(gordonDomain string, ob *onboarding.Handler, proxyHandler http.Handler) http.Handler { | 2218 | + | // gordonDomainOnboardingGate returns a handler that serves onboarding routes |
| 2196 | - | mux := http.NewServeMux() | 2219 | + | // only when the request host matches gordonDomain. For mismatched hosts it |
| 2197 | - | mux.HandleFunc("GET /ca", ob.ServeOnboardingPage) | 2220 | + | // delegates to proxyHandler. gordonDomain must already be canonicalised |
| 2198 | - | mux.HandleFunc("GET /ca.crt", ob.ServeCACert) | 2221 | + | // (trimmed, lowered, trailing dot removed). |
| 2199 | - | mux.HandleFunc("GET /ca.mobileconfig", ob.ServeMobileconfig) | 2222 | + | func gordonDomainOnboardingGate(gordonDomain string, onboardingMux, proxyHandler http.Handler) http.Handler { |
| 2200 | - | mux.HandleFunc("GET /.well-known/gordon/", ob.ServeOnboardingPage) | |||
| 2201 | - | mux.HandleFunc("GET /.well-known/gordon/ca", ob.ServeOnboardingPage) | |||
| 2202 | - | mux.HandleFunc("GET /.well-known/gordon/ca.crt", ob.ServeCACert) | |||
| 2203 | - | mux.HandleFunc("GET /.well-known/gordon/ca.mobileconfig", ob.ServeMobileconfig) | |||
| 2204 | - | ||||
| 2205 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 2223 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 2206 | - | if gordonDomain != "" && requestHostMatches(r.Host, gordonDomain) { | 2224 | + | if gordonDomain != "" && canonicalHostsEqual(r.Host, gordonDomain) { |
| 2207 | - | mux.ServeHTTP(w, r) | 2225 | + | onboardingMux.ServeHTTP(w, r) |
| 2208 | return | 2226 | return | ||
| 2209 | } | 2227 | } | ||
| 2210 | proxyHandler.ServeHTTP(w, r) | 2228 | proxyHandler.ServeHTTP(w, r) | ||
M internal/app/run_http_handlers_test.go +48 -89
| @@ -135,98 +135,50 @@ const directAddr = "203.0.113.10:12345" | |||
| 135 | 135 | // trustedProxyAddr is an IP inside the 173.245.48.0/20 range. | |
| 136 | 136 | const trustedProxyAddr = "173.245.48.5:12345" | |
| 137 | 137 | ||
| 138 | - | func TestCreateHTTPHandlers_DirectHTTPRoot_ServesOnboarding(t *testing.T) { | |
| 138 | + | func TestCreateHTTPHandlers_DirectHTTPOnboarding_AllPaths(t *testing.T) { | |
| 139 | 139 | t.Parallel() | |
| 140 | - | ca := newTestCA(t) | |
| 141 | - | cfg := newOnboardingConfig() | |
| 142 | - | svc := &services{caAdapter: ca} | |
| 143 | - | ||
| 144 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |
| 145 | - | ||
| 146 | - | req := httptest.NewRequest(http.MethodGet, "/", nil) | |
| 147 | - | req.RemoteAddr = directAddr | |
| 148 | - | req.Host = "o2.bnema.dev" | |
| 149 | - | rec := httptest.NewRecorder() | |
| 150 | - | httpHandler.ServeHTTP(rec, req) | |
| 151 | - | ||
| 152 | - | assert.Equal(t, http.StatusOK, rec.Code) | |
| 153 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "text/html") | |
| 154 | - | assert.Contains(t, rec.Body.String(), "Trust CA Certificate") | |
| 155 | - | } | |
| 156 | - | ||
| 157 | - | func TestCreateHTTPHandlers_DirectHTTPWellKnownGordonCACertPath_ServesCACert(t *testing.T) { | |
| 158 | - | t.Parallel() | |
| 159 | - | ca := newTestCA(t) | |
| 160 | - | cfg := newOnboardingConfig() | |
| 161 | - | svc := &services{caAdapter: ca} | |
| 162 | - | ||
| 163 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |
| 164 | - | ||
| 165 | - | req := httptest.NewRequest(http.MethodGet, "/.well-known/gordon/ca.crt", nil) | |
| 166 | - | req.RemoteAddr = directAddr | |
| 167 | - | req.Host = "o2.bnema.dev" | |
| 168 | - | rec := httptest.NewRecorder() | |
| 169 | - | httpHandler.ServeHTTP(rec, req) | |
| 170 | - | ||
| 171 | - | assert.Equal(t, http.StatusOK, rec.Code) | |
| 172 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "application/x-x509-ca-cert") | |
| 173 | - | assert.Contains(t, rec.Body.String(), "BEGIN CERTIFICATE") | |
| 174 | - | } | |
| 175 | - | ||
| 176 | - | func TestCreateHTTPHandlers_DirectHTTPWellKnownGordonOnboarding_ServesOnboarding(t *testing.T) { | |
| 177 | - | t.Parallel() | |
| 178 | - | ca := newTestCA(t) | |
| 179 | - | cfg := newOnboardingConfig() | |
| 180 | - | svc := &services{caAdapter: ca} | |
| 181 | - | ||
| 182 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |
| 183 | - | ||
| 184 | - | req := httptest.NewRequest(http.MethodGet, "/.well-known/gordon/", nil) | |
| 185 | - | req.RemoteAddr = directAddr | |
| 186 | - | req.Host = "o2.bnema.dev" | |
| 187 | - | rec := httptest.NewRecorder() | |
| 188 | - | httpHandler.ServeHTTP(rec, req) | |
| 189 | - | ||
| 190 | - | assert.Equal(t, http.StatusOK, rec.Code) | |
| 191 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "text/html") | |
| 192 | - | assert.Contains(t, rec.Body.String(), "Trust CA Certificate") | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | func TestCreateHTTPHandlers_DirectHTTPCertPath_ServesCACert(t *testing.T) { | |
| 196 | - | t.Parallel() | |
| 197 | - | ca := newTestCA(t) | |
| 198 | - | cfg := newOnboardingConfig() | |
| 199 | - | svc := &services{caAdapter: ca} | |
| 200 | - | ||
| 201 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |
| 202 | - | ||
| 203 | - | req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil) | |
| 204 | - | req.RemoteAddr = directAddr | |
| 205 | - | req.Host = "o2.bnema.dev" | |
| 206 | - | rec := httptest.NewRecorder() | |
| 207 | - | httpHandler.ServeHTTP(rec, req) | |
| 208 | - | ||
| 209 | - | assert.Equal(t, http.StatusOK, rec.Code) | |
| 210 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "application/x-x509-ca-cert") | |
| 211 | - | assert.Contains(t, rec.Body.String(), "BEGIN CERTIFICATE") | |
| 212 | - | } | |
| 213 | 140 | ||
| 214 | - | func TestCreateHTTPHandlers_DirectHTTPUnknownPath_ReturnsForbidden(t *testing.T) { | |
| 215 | - | t.Parallel() | |
| 216 | - | ca := newTestCA(t) | |
| 217 | - | cfg := newOnboardingConfig() | |
| 218 | - | svc := &services{caAdapter: ca} | |
| 219 | - | ||
| 220 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |
| 221 | - | ||
| 222 | - | req := httptest.NewRequest(http.MethodGet, "/web", nil) | |
| 223 | - | req.RemoteAddr = directAddr | |
| 224 | - | req.Host = "o2.bnema.dev" | |
| 225 | - | rec := httptest.NewRecorder() | |
| 226 | - | httpHandler.ServeHTTP(rec, req) | |
| 141 | + | tests := []struct { | |
| 142 | + | name string | |
| 143 | + | path string | |
| 144 | + | wantCode int | |
| 145 | + | wantContent string // substring check | |
| 146 | + | wantType string // Content-Type substring | |
| 147 | + | }{ | |
| 148 | + | {name: "root serves onboarding", path: "/", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, | |
| 149 | + | {name: ".well-known/gordon/ serves onboarding", path: "/.well-known/gordon/", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, | |
| 150 | + | {name: ".well-known/gordon/ca.crt serves CA cert", path: "/.well-known/gordon/ca.crt", wantCode: http.StatusOK, wantContent: "BEGIN CERTIFICATE", wantType: "application/x-x509-ca-cert"}, | |
| 151 | + | {name: "/ca.crt serves CA cert", path: "/ca.crt", wantCode: http.StatusOK, wantContent: "BEGIN CERTIFICATE", wantType: "application/x-x509-ca-cert"}, | |
| 152 | + | {name: "unknown path returns forbidden", path: "/web", wantCode: http.StatusForbidden, wantContent: "Only certificate onboarding is available", wantType: "text/plain"}, | |
| 153 | + | {name: ".well-known/gordon/ca serves onboarding", path: "/.well-known/gordon/ca", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, | |
| 154 | + | {name: "/ca serves onboarding", path: "/ca", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, | |
| 155 | + | {name: "/ca.mobileconfig serves mobileconfig", path: "/ca.mobileconfig", wantCode: http.StatusOK, wantContent: "<?xml", wantType: "application/x-apple-aspen-config"}, | |
| 156 | + | {name: ".well-known/gordon/ca.mobileconfig serves mobileconfig", path: "/.well-known/gordon/ca.mobileconfig", wantCode: http.StatusOK, wantContent: "<?xml", wantType: "application/x-apple-aspen-config"}, | |
| 157 | + | } | |
| 227 | 158 | ||
| 228 | - | assert.Equal(t, http.StatusForbidden, rec.Code) | |
| 229 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "text/plain") | |
| 159 | + | for _, tc := range tests { | |
| 160 | + | t.Run(tc.name, func(t *testing.T) { | |
| 161 | + | ca := newTestCA(t) | |
| 162 | + | cfg := newOnboardingConfig() | |
| 163 | + | svc := &services{caAdapter: ca} | |
| 164 | + | ||
| 165 | + | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |
| 166 | + | ||
| 167 | + | req := httptest.NewRequest(http.MethodGet, tc.path, nil) | |
| 168 | + | req.RemoteAddr = directAddr | |
| 169 | + | req.Host = "o2.bnema.dev" | |
| 170 | + | rec := httptest.NewRecorder() | |
| 171 | + | httpHandler.ServeHTTP(rec, req) | |
| 172 | + | ||
| 173 | + | assert.Equal(t, tc.wantCode, rec.Code, "%s: status code", tc.name) | |
| 174 | + | if tc.wantType != "" { | |
| 175 | + | assert.Contains(t, rec.Header().Get("Content-Type"), tc.wantType, "%s: content type", tc.name) | |
| 176 | + | } | |
| 177 | + | if tc.wantContent != "" { | |
| 178 | + | assert.Contains(t, rec.Body.String(), tc.wantContent, "%s: body content", tc.name) | |
| 179 | + | } | |
| 180 | + | }) | |
| 181 | + | } | |
| 230 | 182 | } | |
| 231 | 183 | ||
| 232 | 184 | func TestCreateHTTPHandlers_TrustedProxyHTTP_DoesNotServeOnboarding(t *testing.T) { | |
| @@ -482,6 +434,13 @@ func TestStartPublicTLSRuntime_ReconcilesAndStartsRenewalLoop(t *testing.T) { | |||
| 482 | 434 | assert.Equal(t, 1, runtime.loopCalls) | |
| 483 | 435 | } | |
| 484 | 436 | ||
| 437 | + | func TestStartPublicTLSRuntime_NilSvc(t *testing.T) { | |
| 438 | + | t.Parallel() | |
| 439 | + | ||
| 440 | + | err := startPublicTLSRuntime(context.Background(), nil, zerowrap.Default()) | |
| 441 | + | require.NoError(t, err) | |
| 442 | + | } | |
| 443 | + | ||
| 485 | 444 | func TestStartPublicTLSRuntime_DoesNotStartRenewalLoopWhenReconcileFails(t *testing.T) { | |
| 486 | 445 | t.Parallel() | |
| 487 | 446 | ||
| @@ -135,98 +135,50 @@ const directAddr = "203.0.113.10:12345" | |||||
| 135 | // trustedProxyAddr is an IP inside the 173.245.48.0/20 range. | 135 | // trustedProxyAddr is an IP inside the 173.245.48.0/20 range. | ||
| 136 | const trustedProxyAddr = "173.245.48.5:12345" | 136 | const trustedProxyAddr = "173.245.48.5:12345" | ||
| 137 | 137 | ||||
| 138 | - | func TestCreateHTTPHandlers_DirectHTTPRoot_ServesOnboarding(t *testing.T) { | 138 | + | func TestCreateHTTPHandlers_DirectHTTPOnboarding_AllPaths(t *testing.T) { |
| 139 | t.Parallel() | 139 | t.Parallel() | ||
| 140 | - | ca := newTestCA(t) | |||
| 141 | - | cfg := newOnboardingConfig() | |||
| 142 | - | svc := &services{caAdapter: ca} | |||
| 143 | - | ||||
| 144 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |||
| 145 | - | ||||
| 146 | - | req := httptest.NewRequest(http.MethodGet, "/", nil) | |||
| 147 | - | req.RemoteAddr = directAddr | |||
| 148 | - | req.Host = "o2.bnema.dev" | |||
| 149 | - | rec := httptest.NewRecorder() | |||
| 150 | - | httpHandler.ServeHTTP(rec, req) | |||
| 151 | - | ||||
| 152 | - | assert.Equal(t, http.StatusOK, rec.Code) | |||
| 153 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "text/html") | |||
| 154 | - | assert.Contains(t, rec.Body.String(), "Trust CA Certificate") | |||
| 155 | - | } | |||
| 156 | - | ||||
| 157 | - | func TestCreateHTTPHandlers_DirectHTTPWellKnownGordonCACertPath_ServesCACert(t *testing.T) { | |||
| 158 | - | t.Parallel() | |||
| 159 | - | ca := newTestCA(t) | |||
| 160 | - | cfg := newOnboardingConfig() | |||
| 161 | - | svc := &services{caAdapter: ca} | |||
| 162 | - | ||||
| 163 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |||
| 164 | - | ||||
| 165 | - | req := httptest.NewRequest(http.MethodGet, "/.well-known/gordon/ca.crt", nil) | |||
| 166 | - | req.RemoteAddr = directAddr | |||
| 167 | - | req.Host = "o2.bnema.dev" | |||
| 168 | - | rec := httptest.NewRecorder() | |||
| 169 | - | httpHandler.ServeHTTP(rec, req) | |||
| 170 | - | ||||
| 171 | - | assert.Equal(t, http.StatusOK, rec.Code) | |||
| 172 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "application/x-x509-ca-cert") | |||
| 173 | - | assert.Contains(t, rec.Body.String(), "BEGIN CERTIFICATE") | |||
| 174 | - | } | |||
| 175 | - | ||||
| 176 | - | func TestCreateHTTPHandlers_DirectHTTPWellKnownGordonOnboarding_ServesOnboarding(t *testing.T) { | |||
| 177 | - | t.Parallel() | |||
| 178 | - | ca := newTestCA(t) | |||
| 179 | - | cfg := newOnboardingConfig() | |||
| 180 | - | svc := &services{caAdapter: ca} | |||
| 181 | - | ||||
| 182 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |||
| 183 | - | ||||
| 184 | - | req := httptest.NewRequest(http.MethodGet, "/.well-known/gordon/", nil) | |||
| 185 | - | req.RemoteAddr = directAddr | |||
| 186 | - | req.Host = "o2.bnema.dev" | |||
| 187 | - | rec := httptest.NewRecorder() | |||
| 188 | - | httpHandler.ServeHTTP(rec, req) | |||
| 189 | - | ||||
| 190 | - | assert.Equal(t, http.StatusOK, rec.Code) | |||
| 191 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "text/html") | |||
| 192 | - | assert.Contains(t, rec.Body.String(), "Trust CA Certificate") | |||
| 193 | - | } | |||
| 194 | - | ||||
| 195 | - | func TestCreateHTTPHandlers_DirectHTTPCertPath_ServesCACert(t *testing.T) { | |||
| 196 | - | t.Parallel() | |||
| 197 | - | ca := newTestCA(t) | |||
| 198 | - | cfg := newOnboardingConfig() | |||
| 199 | - | svc := &services{caAdapter: ca} | |||
| 200 | - | ||||
| 201 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |||
| 202 | - | ||||
| 203 | - | req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil) | |||
| 204 | - | req.RemoteAddr = directAddr | |||
| 205 | - | req.Host = "o2.bnema.dev" | |||
| 206 | - | rec := httptest.NewRecorder() | |||
| 207 | - | httpHandler.ServeHTTP(rec, req) | |||
| 208 | - | ||||
| 209 | - | assert.Equal(t, http.StatusOK, rec.Code) | |||
| 210 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "application/x-x509-ca-cert") | |||
| 211 | - | assert.Contains(t, rec.Body.String(), "BEGIN CERTIFICATE") | |||
| 212 | - | } | |||
| 213 | 140 | ||||
| 214 | - | func TestCreateHTTPHandlers_DirectHTTPUnknownPath_ReturnsForbidden(t *testing.T) { | 141 | + | tests := []struct { |
| 215 | - | t.Parallel() | 142 | + | name string |
| 216 | - | ca := newTestCA(t) | 143 | + | path string |
| 217 | - | cfg := newOnboardingConfig() | 144 | + | wantCode int |
| 218 | - | svc := &services{caAdapter: ca} | 145 | + | wantContent string // substring check |
| 219 | - | 146 | + | wantType string // Content-Type substring | |
| 220 | - | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | 147 | + | }{ |
| 221 | - | 148 | + | {name: "root serves onboarding", path: "/", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, | |
| 222 | - | req := httptest.NewRequest(http.MethodGet, "/web", nil) | 149 | + | {name: ".well-known/gordon/ serves onboarding", path: "/.well-known/gordon/", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, |
| 223 | - | req.RemoteAddr = directAddr | 150 | + | {name: ".well-known/gordon/ca.crt serves CA cert", path: "/.well-known/gordon/ca.crt", wantCode: http.StatusOK, wantContent: "BEGIN CERTIFICATE", wantType: "application/x-x509-ca-cert"}, |
| 224 | - | req.Host = "o2.bnema.dev" | 151 | + | {name: "/ca.crt serves CA cert", path: "/ca.crt", wantCode: http.StatusOK, wantContent: "BEGIN CERTIFICATE", wantType: "application/x-x509-ca-cert"}, |
| 225 | - | rec := httptest.NewRecorder() | 152 | + | {name: "unknown path returns forbidden", path: "/web", wantCode: http.StatusForbidden, wantContent: "Only certificate onboarding is available", wantType: "text/plain"}, |
| 226 | - | httpHandler.ServeHTTP(rec, req) | 153 | + | {name: ".well-known/gordon/ca serves onboarding", path: "/.well-known/gordon/ca", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, |
| 154 | + | {name: "/ca serves onboarding", path: "/ca", wantCode: http.StatusOK, wantContent: "Trust CA Certificate", wantType: "text/html"}, | |||
| 155 | + | {name: "/ca.mobileconfig serves mobileconfig", path: "/ca.mobileconfig", wantCode: http.StatusOK, wantContent: "<?xml", wantType: "application/x-apple-aspen-config"}, | |||
| 156 | + | {name: ".well-known/gordon/ca.mobileconfig serves mobileconfig", path: "/.well-known/gordon/ca.mobileconfig", wantCode: http.StatusOK, wantContent: "<?xml", wantType: "application/x-apple-aspen-config"}, | |||
| 157 | + | } | |||
| 227 | 158 | ||||
| 228 | - | assert.Equal(t, http.StatusForbidden, rec.Code) | 159 | + | for _, tc := range tests { |
| 229 | - | assert.Contains(t, rec.Header().Get("Content-Type"), "text/plain") | 160 | + | t.Run(tc.name, func(t *testing.T) { |
| 161 | + | ca := newTestCA(t) | |||
| 162 | + | cfg := newOnboardingConfig() | |||
| 163 | + | svc := &services{caAdapter: ca} | |||
| 164 | + | ||||
| 165 | + | _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil) | |||
| 166 | + | ||||
| 167 | + | req := httptest.NewRequest(http.MethodGet, tc.path, nil) | |||
| 168 | + | req.RemoteAddr = directAddr | |||
| 169 | + | req.Host = "o2.bnema.dev" | |||
| 170 | + | rec := httptest.NewRecorder() | |||
| 171 | + | httpHandler.ServeHTTP(rec, req) | |||
| 172 | + | ||||
| 173 | + | assert.Equal(t, tc.wantCode, rec.Code, "%s: status code", tc.name) | |||
| 174 | + | if tc.wantType != "" { | |||
| 175 | + | assert.Contains(t, rec.Header().Get("Content-Type"), tc.wantType, "%s: content type", tc.name) | |||
| 176 | + | } | |||
| 177 | + | if tc.wantContent != "" { | |||
| 178 | + | assert.Contains(t, rec.Body.String(), tc.wantContent, "%s: body content", tc.name) | |||
| 179 | + | } | |||
| 180 | + | }) | |||
| 181 | + | } | |||
| 230 | } | 182 | } | ||
| 231 | 183 | ||||
| 232 | func TestCreateHTTPHandlers_TrustedProxyHTTP_DoesNotServeOnboarding(t *testing.T) { | 184 | func TestCreateHTTPHandlers_TrustedProxyHTTP_DoesNotServeOnboarding(t *testing.T) { | ||
| @@ -482,6 +434,13 @@ func TestStartPublicTLSRuntime_ReconcilesAndStartsRenewalLoop(t *testing.T) { | |||||
| 482 | assert.Equal(t, 1, runtime.loopCalls) | 434 | assert.Equal(t, 1, runtime.loopCalls) | ||
| 483 | } | 435 | } | ||
| 484 | 436 | ||||
| 437 | + | func TestStartPublicTLSRuntime_NilSvc(t *testing.T) { | |||
| 438 | + | t.Parallel() | |||
| 439 | + | ||||
| 440 | + | err := startPublicTLSRuntime(context.Background(), nil, zerowrap.Default()) | |||
| 441 | + | require.NoError(t, err) | |||
| 442 | + | } | |||
| 443 | + | ||||
| 485 | func TestStartPublicTLSRuntime_DoesNotStartRenewalLoopWhenReconcileFails(t *testing.T) { | 444 | func TestStartPublicTLSRuntime_DoesNotStartRenewalLoopWhenReconcileFails(t *testing.T) { | ||
| 486 | t.Parallel() | 445 | t.Parallel() | ||
| 487 | 446 | ||||
M internal/usecase/publictls/service.go +11 -14
| @@ -184,19 +184,26 @@ func (s *Service) Reconcile(ctx context.Context) error { | |||
| 184 | 184 | s.mu.Lock() | |
| 185 | 185 | s.requiredHosts = required | |
| 186 | 186 | ||
| 187 | - | missing := make([]CertificateTarget, 0) | |
| 187 | + | batchSize := s.cfg.ObtainBatchSize | |
| 188 | + | if batchSize <= 0 { | |
| 189 | + | batchSize = defaultObtainBatchSize | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | var missing []CertificateTarget | |
| 193 | + | pendingCount := 0 | |
| 188 | 194 | for _, target := range targets { | |
| 189 | 195 | if s.certificateExistsLocked(target) { | |
| 190 | 196 | continue | |
| 191 | 197 | } | |
| 192 | - | missing = append(missing, target) | |
| 198 | + | pendingCount++ | |
| 199 | + | if len(missing) < batchSize { | |
| 200 | + | missing = append(missing, target) | |
| 201 | + | } | |
| 193 | 202 | } | |
| 194 | 203 | s.mu.Unlock() | |
| 195 | 204 | ||
| 196 | 205 | // Process missing targets without holding s.mu or the store lock so | |
| 197 | 206 | // GetCertificate/Status are not blocked during network I/O (Obtain). | |
| 198 | - | pendingCount := len(missing) | |
| 199 | - | missing = limitMissingTargets(missing, s.cfg.ObtainBatchSize) | |
| 200 | 207 | if remaining := pendingCount - len(missing); remaining > 0 { | |
| 201 | 208 | log.Info(). | |
| 202 | 209 | Int("obtain_batch_size", len(missing)). | |
| @@ -209,16 +216,6 @@ func (s *Service) Reconcile(ctx context.Context) error { | |||
| 209 | 216 | return nil | |
| 210 | 217 | } | |
| 211 | 218 | ||
| 212 | - | func limitMissingTargets(targets []CertificateTarget, batchSize int) []CertificateTarget { | |
| 213 | - | if batchSize <= 0 { | |
| 214 | - | batchSize = defaultObtainBatchSize | |
| 215 | - | } | |
| 216 | - | if len(targets) <= batchSize { | |
| 217 | - | return targets | |
| 218 | - | } | |
| 219 | - | return targets[:batchSize] | |
| 220 | - | } | |
| 221 | - | ||
| 222 | 219 | func populateStoredCertificate(cert *out.StoredCertificate) error { | |
| 223 | 220 | if cert == nil { | |
| 224 | 221 | return fmt.Errorf("stored certificate is nil") | |
| @@ -184,19 +184,26 @@ func (s *Service) Reconcile(ctx context.Context) error { | |||||
| 184 | s.mu.Lock() | 184 | s.mu.Lock() | ||
| 185 | s.requiredHosts = required | 185 | s.requiredHosts = required | ||
| 186 | 186 | ||||
| 187 | - | missing := make([]CertificateTarget, 0) | 187 | + | batchSize := s.cfg.ObtainBatchSize |
| 188 | + | if batchSize <= 0 { | |||
| 189 | + | batchSize = defaultObtainBatchSize | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | var missing []CertificateTarget | |||
| 193 | + | pendingCount := 0 | |||
| 188 | for _, target := range targets { | 194 | for _, target := range targets { | ||
| 189 | if s.certificateExistsLocked(target) { | 195 | if s.certificateExistsLocked(target) { | ||
| 190 | continue | 196 | continue | ||
| 191 | } | 197 | } | ||
| 192 | - | missing = append(missing, target) | 198 | + | pendingCount++ |
| 199 | + | if len(missing) < batchSize { | |||
| 200 | + | missing = append(missing, target) | |||
| 201 | + | } | |||
| 193 | } | 202 | } | ||
| 194 | s.mu.Unlock() | 203 | s.mu.Unlock() | ||
| 195 | 204 | ||||
| 196 | // Process missing targets without holding s.mu or the store lock so | 205 | // Process missing targets without holding s.mu or the store lock so | ||
| 197 | // GetCertificate/Status are not blocked during network I/O (Obtain). | 206 | // GetCertificate/Status are not blocked during network I/O (Obtain). | ||
| 198 | - | pendingCount := len(missing) | |||
| 199 | - | missing = limitMissingTargets(missing, s.cfg.ObtainBatchSize) | |||
| 200 | if remaining := pendingCount - len(missing); remaining > 0 { | 207 | if remaining := pendingCount - len(missing); remaining > 0 { | ||
| 201 | log.Info(). | 208 | log.Info(). | ||
| 202 | Int("obtain_batch_size", len(missing)). | 209 | Int("obtain_batch_size", len(missing)). | ||
| @@ -209,16 +216,6 @@ func (s *Service) Reconcile(ctx context.Context) error { | |||||
| 209 | return nil | 216 | return nil | ||
| 210 | } | 217 | } | ||
| 211 | 218 | ||||
| 212 | - | func limitMissingTargets(targets []CertificateTarget, batchSize int) []CertificateTarget { | |||
| 213 | - | if batchSize <= 0 { | |||
| 214 | - | batchSize = defaultObtainBatchSize | |||
| 215 | - | } | |||
| 216 | - | if len(targets) <= batchSize { | |||
| 217 | - | return targets | |||
| 218 | - | } | |||
| 219 | - | return targets[:batchSize] | |||
| 220 | - | } | |||
| 221 | - | ||||
| 222 | func populateStoredCertificate(cert *out.StoredCertificate) error { | 219 | func populateStoredCertificate(cert *out.StoredCertificate) error { | ||
| 223 | if cert == nil { | 220 | if cert == nil { | ||
| 224 | return fmt.Errorf("stored certificate is nil") | 221 | return fmt.Errorf("stored certificate is nil") | ||