detached 14736d4

feat(pki): wire internal CA into server — dual HTTP/HTTPS handler chains, on-demand TLS

commit
14736d4c10dd50db1530c932d996aa6cc98b9289
author
brice · · 2026-03-29 17:00 UTC
parent
1659415
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +122 -62
M internal/app/run.go +122 -62
@@ -32,6 +32,7 @@ import (
3232 "github.com/bnema/gordon/internal/adapters/out/filesystem"
3333 "github.com/bnema/gordon/internal/adapters/out/httpprober"
3434 "github.com/bnema/gordon/internal/adapters/out/logwriter"
35+ pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
3536 "github.com/bnema/gordon/internal/adapters/out/ratelimit"
3637 "github.com/bnema/gordon/internal/adapters/out/secrets"
3738 "github.com/bnema/gordon/internal/adapters/out/telemetry"
@@ -45,6 +46,7 @@ import (
4546 "github.com/bnema/gordon/internal/adapters/in/http/admin"
4647 authhandler "github.com/bnema/gordon/internal/adapters/in/http/auth"
4748 "github.com/bnema/gordon/internal/adapters/in/http/middleware"
49+ "github.com/bnema/gordon/internal/adapters/in/http/onboarding"
4850 proxyadapter "github.com/bnema/gordon/internal/adapters/in/http/proxy"
4951 "github.com/bnema/gordon/internal/adapters/in/http/registry"
5052
@@ -68,6 +70,7 @@ import (
6870 "github.com/bnema/gordon/internal/usecase/health"
6971 "github.com/bnema/gordon/internal/usecase/images"
7072 "github.com/bnema/gordon/internal/usecase/logs"
73+ pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
7174 "github.com/bnema/gordon/internal/usecase/proxy"
7275 registrySvc "github.com/bnema/gordon/internal/usecase/registry"
7376 secretsSvc "github.com/bnema/gordon/internal/usecase/secrets"
@@ -197,6 +200,9 @@ type services struct {
197200 previewService *preview.Service
198201 envDir string
199202 maxBlobChunkSize int64
203+ caAdapter *pkiadapter.CA
204+ pkiSvc *pkiusecase.Service
205+ proxyAllowedNets []*net.IPNet // Cloudflare CIDRs for onboarding handler
200206 }
201207
202208 // Run initializes and starts the Gordon application.
@@ -278,11 +284,8 @@ func Run(ctx context.Context, configPath string) error {
278284 }
279285 defer svc.eventBus.Stop()
280286
281- // Create HTTP handlers
282- registryHandler, proxyHandler, proxyCIDRMiddleware := createHTTPHandlers(svc, cfg, log)
283-
284287 // Start servers, wait for listeners to bind, then sync/auto-start containers.
285- return runServers(ctx, v, cfg, registryHandler, proxyHandler, proxyCIDRMiddleware, svc, cleanupHandlers, log)
288+ return runServers(ctx, v, cfg, svc, cleanupHandlers, log)
286289 }
287290
288291 // initConfig loads configuration from file.
@@ -373,6 +376,14 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
373376 return nil, log.WrapErr(err, "failed to load configuration")
374377 }
375378
379+ // Create PKI (internal CA)
380+ caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)
381+ if err != nil {
382+ return nil, log.WrapErr(err, "failed to initialize internal CA")
383+ }
384+ svc.caAdapter = caAdapter
385+ svc.pkiSvc = pkiusecase.NewService(ctx, caAdapter, svc.configSvc, log)
386+
376387 var backend domain.SecretsBackend
377388 var passStore *domainsecrets.PassStore
378389 var domainSecretStore out.DomainSecretStore
@@ -1545,13 +1556,13 @@ func loopbackOnly(next http.Handler, log zerowrap.Logger) http.Handler {
15451556 }
15461557
15471558 // createHTTPHandlers creates HTTP handlers with middleware.
1548-// The returned proxyCIDRMiddleware is non-nil when proxy_allowed_ips is configured;
1549-// callers must also apply it to the HTTP-to-HTTPS redirect listener.
1550-func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler, func(http.Handler) http.Handler) {
1559+// Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.
1560+func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler, http.Handler) {
15511561 // Parse trusted proxies once for all middleware chains.
15521562 // This ensures consistent IP extraction across logging, rate limiting, and auth.
15531563 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
15541564
1565+ // Registry handler (unchanged)
15551566 registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize)
15561567 registryWithMiddleware, cidrAllowlistMiddleware, rateLimitMiddleware := buildRegistryHandlerWithMiddleware(
15571568 svc,
@@ -1564,36 +1575,57 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
15641575 registryMux := http.NewServeMux()
15651576 registerAuthRoutes(registryMux, svc, trustedNets, cidrAllowlistMiddleware, rateLimitMiddleware, cfg, log)
15661577 registryMux.Handle("/v2/", wrapRegistryForLocalMode(registryWithMiddleware, cfg, log))
1578+ registerAdminRoutes(registryMux, svc, cfg, trustedNets, log)
15671579
1568- // SECURITY: No CORS middleware on the proxy chain. Backend applications
1569- // should control their own CORS policies. A blanket Access-Control-Allow-Origin: *
1570- // would override backend CORS settings and allow any website to make
1571- // cross-origin authenticated requests to proxied applications.
1572- proxyMiddlewares := []func(http.Handler) http.Handler{
1580+ // Proxy handler
1581+ proxyHandler := proxyadapter.NewHandler(svc.proxySvc, log)
1582+
1583+ // HTTP proxy handler chain: CIDR + onboarding routes + proxy
1584+ var proxyAllowedNets []*net.IPNet
1585+ var proxyCIDRMiddleware func(http.Handler) http.Handler
1586+ proxyAllowedNets, proxyCIDRMiddleware = buildProxyCIDRAllowlistMiddleware(cfg, trustedNets, log)
1587+ svc.proxyAllowedNets = proxyAllowedNets
1588+
1589+ httpProxyMiddlewares := []func(http.Handler) http.Handler{
15731590 middleware.PanicRecovery(log),
15741591 middleware.RequestLogger(log, trustedNets),
1575- middleware.SecurityHeaders,
15761592 }
1577-
1578- // SECURITY: When proxy_allowed_ips is set, only connections from those CIDRs
1579- // can reach the proxy. This prevents direct-to-origin attacks that bypass
1580- // CDN protections (e.g. Cloudflare WAF, DDoS mitigation).
1581- proxyCIDRMiddleware := buildProxyCIDRAllowlistMiddleware(cfg, trustedNets, log)
15821593 if proxyCIDRMiddleware != nil {
1583- proxyMiddlewares = append(proxyMiddlewares, proxyCIDRMiddleware)
1594+ httpProxyMiddlewares = append(httpProxyMiddlewares, proxyCIDRMiddleware)
15841595 }
15851596
1586- proxyHandler := proxyadapter.NewHandler(svc.proxySvc, log)
1587- proxyWithMiddleware := otelhttp.NewHandler(
1588- middleware.Chain(proxyMiddlewares...)(proxyHandler),
1597+ httpProxyWithMiddleware := otelhttp.NewHandler(
1598+ middleware.Chain(httpProxyMiddlewares...)(proxyHandler),
15891599 "gordon.proxy",
15901600 )
1601+
1602+ // Build proxyMux with onboarding routes before catch-all
15911603 proxyMux := http.NewServeMux()
1592- proxyMux.Handle("/", proxyWithMiddleware)
1604+ if svc.caAdapter != nil {
1605+ onboardingHandler := onboarding.NewHandler(
1606+ svc.caAdapter.RootCertificate(),
1607+ svc.caAdapter.RootCertificateDER(),
1608+ svc.caAdapter.RootCommonName(),
1609+ svc.proxyAllowedNets,
1610+ )
1611+ proxyMux.HandleFunc("GET /ca", onboardingHandler.ServeOnboardingPage)
1612+ proxyMux.HandleFunc("GET /ca.crt", onboardingHandler.ServeCACert)
1613+ proxyMux.HandleFunc("GET /ca.mobileconfig", onboardingHandler.ServeMobileconfig)
1614+ }
1615+ proxyMux.Handle("/", httpProxyWithMiddleware)
15931616
1594- registerAdminRoutes(registryMux, svc, cfg, trustedNets, log)
1617+ // HTTPS proxy handler chain: security headers + proxy (no CIDR, no onboarding)
1618+ httpsProxyMiddlewares := []func(http.Handler) http.Handler{
1619+ middleware.PanicRecovery(log),
1620+ middleware.RequestLogger(log, trustedNets),
1621+ middleware.SecurityHeaders,
1622+ }
1623+ httpsHandler := otelhttp.NewHandler(
1624+ middleware.Chain(httpsProxyMiddlewares...)(proxyHandler),
1625+ "gordon.proxy.tls",
1626+ )
15951627
1596- return registryMux, proxyMux, proxyCIDRMiddleware
1628+ return registryMux, proxyMux, httpsHandler
15971629 }
15981630
15991631 func buildRegistryHandlerWithMiddleware(
@@ -1677,20 +1709,20 @@ func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet,
16771709 return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)
16781710 }
16791711
1680-func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {
1712+func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) ([]*net.IPNet, func(http.Handler) http.Handler) {
16811713 allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.ProxyAllowedIPs, "proxy_allowed_ips", log)
16821714 if allInvalid {
1683- return denyAllHandler("proxy_allowed_ips", trustedNets, log)
1715+ return nil, denyAllHandler("proxy_allowed_ips", trustedNets, log)
16841716 }
16851717 if allowedNets == nil {
1686- return nil
1718+ return nil, nil
16871719 }
16881720
16891721 log.Info().
16901722 Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).
16911723 Msg("proxy origin IP allowlist enabled")
16921724
1693- return middleware.ProxyCIDRAllowlist(allowedNets, log)
1725+ return allowedNets, middleware.ProxyCIDRAllowlist(allowedNets, log)
16941726 }
16951727
16961728 func buildRegistryRateLimitMiddleware(cfg Config, log zerowrap.Logger) func(http.Handler) http.Handler {
@@ -1825,7 +1857,7 @@ func registerAdminRoutes(registryMux *http.ServeMux, svc *services, cfg Config,
18251857 // - SIGUSR2: Triggers manual deploy for a specific route
18261858 // The deferred signal.Stop calls ensure signal handlers are properly
18271859 // cleaned up before program exit, preventing signal handler leaks.
1828-func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler, proxyHandler http.Handler, proxyCIDRMiddleware func(http.Handler) http.Handler, svc *services, cleanupHandlers func(), log zerowrap.Logger) error {
1860+func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services, cleanupHandlers func(), log zerowrap.Logger) error {
18291861 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
18301862 defer cancel()
18311863
@@ -1843,15 +1875,17 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
18431875
18441876 errChan := make(chan error, 3)
18451877
1878+ registryHandler, httpProxyHandler, httpsProxyHandler := createHTTPHandlers(svc, cfg, log)
1879+
18461880 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))
18471881 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)
18481882
1849- proxySrv, proxyReady, _, _, err := startProxyServers(cfg, proxyHandler, proxyCIDRMiddleware, errChan, log)
1883+ proxySrv, proxyReady, tlsSrv, tlsReady, err := startProxyServers(cfg, httpProxyHandler, httpsProxyHandler, svc.pkiSvc, errChan, log)
18501884 if err != nil {
18511885 return err
18521886 }
18531887
1854- // Wait for all enabled servers to bind their ports before auto-starting containers.
1888+ // Wait for all servers to bind their ports before auto-starting containers.
18551889 // This prevents the race where auto-start pulls from the registry before it's listening.
18561890 if err := waitForServerReady(registryReady, errChan); err != nil {
18571891 return err
@@ -1859,9 +1893,13 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
18591893 if err := waitForServerReady(proxyReady, errChan); err != nil {
18601894 return err
18611895 }
1896+ if err := waitForServerReady(tlsReady, errChan); err != nil {
1897+ return err
1898+ }
18621899
18631900 log.Info().
18641901 Int("proxy_port", cfg.Server.Port).
1902+ Int("tls_port", cfg.Server.TLSPort).
18651903 Int("registry_port", cfg.Server.RegistryPort).
18661904 Msg("Gordon is running")
18671905
@@ -1878,7 +1916,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
18781916
18791917 waitForShutdown(ctx, errChan, reloadChan, deployChan, svc.eventBus, log)
18801918 cleanupHandlers() // Stop debounce timers before draining containers
1881- gracefulShutdown(registrySrv, proxySrv, nil, svc.containerSvc, svc.proxySvc, log)
1919+ gracefulShutdown(registrySrv, proxySrv, tlsSrv, svc.containerSvc, svc.proxySvc, svc.pkiSvc, log)
18821920 return nil
18831921 }
18841922
@@ -2080,7 +2118,7 @@ func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, depl
20802118
20812119 // gracefulShutdown stops HTTP servers with a 30s timeout, then shuts down
20822120 // the container service and cleans up runtime files.
2083-func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, proxySvc *proxy.Service, log zerowrap.Logger) {
2121+func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, proxySvc *proxy.Service, pkiSvc *pkiusecase.Service, log zerowrap.Logger) {
20842122 log.Info().Msg("shutting down Gordon...")
20852123
20862124 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -2096,6 +2134,11 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
20962134 }
20972135 }
20982136
2137+ // Stop PKI maintenance goroutines
2138+ if pkiSvc != nil {
2139+ pkiSvc.Stop()
2140+ }
2141+
20992142 // Phase 2: Drain in-flight registry push sessions before stopping the backend
21002143 if proxySvc != nil {
21012144 log.Info().Msg("draining in-flight registry requests...")
@@ -2121,14 +2164,38 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
21212164 log.Info().Msg("Gordon stopped")
21222165 }
21232166
2124-// startProxyServers sets up the proxy HTTP server.
2125-// Task 9 will add the TLS server back with PKI-based certificates.
2126-func startProxyServers(cfg Config, proxyHandler http.Handler, proxyCIDRMiddleware func(http.Handler) http.Handler, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {
2127- var proxyProtos http.Protocols
2128- proxyProtos.SetHTTP1(true)
2129- proxyProtos.SetUnencryptedHTTP2(true)
2130- srv, ready := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", &proxyProtos, errChan, log)
2131- return srv, ready, nil, nil, nil
2167+// startProxyServers sets up the HTTP proxy server and the HTTPS proxy server
2168+// with on-demand TLS certificates from the internal CA.
2169+func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSvc *pkiusecase.Service, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {
2170+ // HTTP listener (Cloudflare proxy + onboarding for direct clients)
2171+ var httpProtos http.Protocols
2172+ httpProtos.SetHTTP1(true)
2173+ httpProtos.SetUnencryptedHTTP2(true)
2174+ httpSrv, httpReady := startServer(
2175+ fmt.Sprintf(":%d", cfg.Server.Port),
2176+ httpHandler,
2177+ "proxy-http",
2178+ &httpProtos,
2179+ errChan,
2180+ log,
2181+ )
2182+
2183+ // HTTPS listener (on-demand TLS via internal CA)
2184+ tlsConfig := &tls.Config{
2185+ MinVersion: tls.VersionTLS12,
2186+ GetCertificate: pkiSvc.GetCertificate,
2187+ NextProtos: []string{"h2", "http/1.1"},
2188+ }
2189+ tlsSrv, tlsReady := startTLSServerWithConfig(
2190+ fmt.Sprintf(":%d", cfg.Server.TLSPort),
2191+ httpsHandler,
2192+ "proxy-tls",
2193+ tlsConfig,
2194+ errChan,
2195+ log,
2196+ )
2197+
2198+ return httpSrv, httpReady, tlsSrv, tlsReady, nil
21322199 }
21332200
21342201 // startServer starts an HTTP server, returning the server instance and a channel
@@ -2168,44 +2235,37 @@ func startServer(addr string, handler http.Handler, name string, protocols *http
21682235 return server, ready
21692236 }
21702237
2171-// startTLSServer starts an HTTPS server with the provided certificate and key.
2172-// It returns the server instance (for graceful shutdown) and a channel that closes once the port is bound.
2173-func startTLSServer(addr string, handler http.Handler, name, certFile, keyFile string, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}) {
2174- ready := make(chan struct{})
2175-
2176- server := &http.Server{
2238+// startTLSServerWithConfig starts an HTTPS server using a pre-built tls.Config
2239+// (e.g. with GetCertificate for on-demand issuance via the internal CA).
2240+// Returns the server instance and a channel that closes once the port is bound.
2241+func startTLSServerWithConfig(addr string, handler http.Handler, name string, tlsCfg *tls.Config, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}) {
2242+ srv := &http.Server{
21772243 Addr: addr,
21782244 Handler: handler,
2245+ TLSConfig: tlsCfg,
21792246 ReadHeaderTimeout: 10 * time.Second,
21802247 ReadTimeout: 5 * time.Minute,
21812248 WriteTimeout: 5 * time.Minute,
21822249 IdleTimeout: 120 * time.Second,
21832250 MaxHeaderBytes: 1 << 20,
2184- TLSConfig: &tls.Config{
2185- MinVersion: tls.VersionTLS12,
2186- },
21872251 }
21882252
2253+ ready := make(chan struct{})
21892254 go func() {
2190- log.Info().
2191- Str("address", addr).
2192- Str("cert_file", certFile).
2193- Str("key_file", keyFile).
2194- Msgf("%s server starting", name)
2195-
21962255 ln, err := net.Listen("tcp", addr)
21972256 if err != nil {
2198- errChan <- fmt.Errorf("%s server error: %w", name, err)
2257+ errChan <- fmt.Errorf("%s listen: %w", name, err)
21992258 return
22002259 }
2260+ tlsLn := tls.NewListener(ln, tlsCfg)
2261+ log.Info().Str("addr", addr).Str("name", name).Msg("TLS server starting")
22012262 close(ready)
2202-
2203- if err := server.ServeTLS(ln, certFile, keyFile); err != nil && err != http.ErrServerClosed {
2204- errChan <- fmt.Errorf("%s server error: %w", name, err)
2263+ if err := srv.Serve(tlsLn); err != nil && err != http.ErrServerClosed {
2264+ errChan <- fmt.Errorf("%s: %w", name, err)
22052265 }
22062266 }()
22072267
2208- return server, ready
2268+ return srv, ready
22092269 }
22102270
22112271 // SendReloadSignal sends SIGUSR1 to the running Gordon process.
@@ -32,6 +32,7 @@ import (
32 "github.com/bnema/gordon/internal/adapters/out/filesystem"32 "github.com/bnema/gordon/internal/adapters/out/filesystem"
33 "github.com/bnema/gordon/internal/adapters/out/httpprober"33 "github.com/bnema/gordon/internal/adapters/out/httpprober"
34 "github.com/bnema/gordon/internal/adapters/out/logwriter"34 "github.com/bnema/gordon/internal/adapters/out/logwriter"
35+ pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
35 "github.com/bnema/gordon/internal/adapters/out/ratelimit"36 "github.com/bnema/gordon/internal/adapters/out/ratelimit"
36 "github.com/bnema/gordon/internal/adapters/out/secrets"37 "github.com/bnema/gordon/internal/adapters/out/secrets"
37 "github.com/bnema/gordon/internal/adapters/out/telemetry"38 "github.com/bnema/gordon/internal/adapters/out/telemetry"
@@ -45,6 +46,7 @@ import (
45 "github.com/bnema/gordon/internal/adapters/in/http/admin"46 "github.com/bnema/gordon/internal/adapters/in/http/admin"
46 authhandler "github.com/bnema/gordon/internal/adapters/in/http/auth"47 authhandler "github.com/bnema/gordon/internal/adapters/in/http/auth"
47 "github.com/bnema/gordon/internal/adapters/in/http/middleware"48 "github.com/bnema/gordon/internal/adapters/in/http/middleware"
49+ "github.com/bnema/gordon/internal/adapters/in/http/onboarding"
48 proxyadapter "github.com/bnema/gordon/internal/adapters/in/http/proxy"50 proxyadapter "github.com/bnema/gordon/internal/adapters/in/http/proxy"
49 "github.com/bnema/gordon/internal/adapters/in/http/registry"51 "github.com/bnema/gordon/internal/adapters/in/http/registry"
50 52
@@ -68,6 +70,7 @@ import (
68 "github.com/bnema/gordon/internal/usecase/health"70 "github.com/bnema/gordon/internal/usecase/health"
69 "github.com/bnema/gordon/internal/usecase/images"71 "github.com/bnema/gordon/internal/usecase/images"
70 "github.com/bnema/gordon/internal/usecase/logs"72 "github.com/bnema/gordon/internal/usecase/logs"
73+ pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
71 "github.com/bnema/gordon/internal/usecase/proxy"74 "github.com/bnema/gordon/internal/usecase/proxy"
72 registrySvc "github.com/bnema/gordon/internal/usecase/registry"75 registrySvc "github.com/bnema/gordon/internal/usecase/registry"
73 secretsSvc "github.com/bnema/gordon/internal/usecase/secrets"76 secretsSvc "github.com/bnema/gordon/internal/usecase/secrets"
@@ -197,6 +200,9 @@ type services struct {
197 previewService *preview.Service200 previewService *preview.Service
198 envDir string201 envDir string
199 maxBlobChunkSize int64202 maxBlobChunkSize int64
203+ caAdapter *pkiadapter.CA
204+ pkiSvc *pkiusecase.Service
205+ proxyAllowedNets []*net.IPNet // Cloudflare CIDRs for onboarding handler
200 }206 }
201 207
202 // Run initializes and starts the Gordon application.208 // Run initializes and starts the Gordon application.
@@ -278,11 +284,8 @@ func Run(ctx context.Context, configPath string) error {
278 }284 }
279 defer svc.eventBus.Stop()285 defer svc.eventBus.Stop()
280 286
281- // Create HTTP handlers
282- registryHandler, proxyHandler, proxyCIDRMiddleware := createHTTPHandlers(svc, cfg, log)
283-
284 // Start servers, wait for listeners to bind, then sync/auto-start containers.287 // Start servers, wait for listeners to bind, then sync/auto-start containers.
285- return runServers(ctx, v, cfg, registryHandler, proxyHandler, proxyCIDRMiddleware, svc, cleanupHandlers, log)288+ return runServers(ctx, v, cfg, svc, cleanupHandlers, log)
286 }289 }
287 290
288 // initConfig loads configuration from file.291 // initConfig loads configuration from file.
@@ -373,6 +376,14 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
373 return nil, log.WrapErr(err, "failed to load configuration")376 return nil, log.WrapErr(err, "failed to load configuration")
374 }377 }
375 378
379+ // Create PKI (internal CA)
380+ caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)
381+ if err != nil {
382+ return nil, log.WrapErr(err, "failed to initialize internal CA")
383+ }
384+ svc.caAdapter = caAdapter
385+ svc.pkiSvc = pkiusecase.NewService(ctx, caAdapter, svc.configSvc, log)
386+
376 var backend domain.SecretsBackend387 var backend domain.SecretsBackend
377 var passStore *domainsecrets.PassStore388 var passStore *domainsecrets.PassStore
378 var domainSecretStore out.DomainSecretStore389 var domainSecretStore out.DomainSecretStore
@@ -1545,13 +1556,13 @@ func loopbackOnly(next http.Handler, log zerowrap.Logger) http.Handler {
1545 }1556 }
1546 1557
1547 // createHTTPHandlers creates HTTP handlers with middleware.1558 // createHTTPHandlers creates HTTP handlers with middleware.
1548-// The returned proxyCIDRMiddleware is non-nil when proxy_allowed_ips is configured;1559+// Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.
1549-// callers must also apply it to the HTTP-to-HTTPS redirect listener.1560+func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler, http.Handler) {
1550-func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler, func(http.Handler) http.Handler) {
1551 // Parse trusted proxies once for all middleware chains.1561 // Parse trusted proxies once for all middleware chains.
1552 // This ensures consistent IP extraction across logging, rate limiting, and auth.1562 // This ensures consistent IP extraction across logging, rate limiting, and auth.
1553 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)1563 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
1554 1564
1565+ // Registry handler (unchanged)
1555 registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize)1566 registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize)
1556 registryWithMiddleware, cidrAllowlistMiddleware, rateLimitMiddleware := buildRegistryHandlerWithMiddleware(1567 registryWithMiddleware, cidrAllowlistMiddleware, rateLimitMiddleware := buildRegistryHandlerWithMiddleware(
1557 svc,1568 svc,
@@ -1564,36 +1575,57 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
1564 registryMux := http.NewServeMux()1575 registryMux := http.NewServeMux()
1565 registerAuthRoutes(registryMux, svc, trustedNets, cidrAllowlistMiddleware, rateLimitMiddleware, cfg, log)1576 registerAuthRoutes(registryMux, svc, trustedNets, cidrAllowlistMiddleware, rateLimitMiddleware, cfg, log)
1566 registryMux.Handle("/v2/", wrapRegistryForLocalMode(registryWithMiddleware, cfg, log))1577 registryMux.Handle("/v2/", wrapRegistryForLocalMode(registryWithMiddleware, cfg, log))
1578+ registerAdminRoutes(registryMux, svc, cfg, trustedNets, log)
1567 1579
1568- // SECURITY: No CORS middleware on the proxy chain. Backend applications1580+ // Proxy handler
1569- // should control their own CORS policies. A blanket Access-Control-Allow-Origin: *1581+ proxyHandler := proxyadapter.NewHandler(svc.proxySvc, log)
1570- // would override backend CORS settings and allow any website to make1582+
1571- // cross-origin authenticated requests to proxied applications.1583+ // HTTP proxy handler chain: CIDR + onboarding routes + proxy
1572- proxyMiddlewares := []func(http.Handler) http.Handler{1584+ var proxyAllowedNets []*net.IPNet
1585+ var proxyCIDRMiddleware func(http.Handler) http.Handler
1586+ proxyAllowedNets, proxyCIDRMiddleware = buildProxyCIDRAllowlistMiddleware(cfg, trustedNets, log)
1587+ svc.proxyAllowedNets = proxyAllowedNets
1588+
1589+ httpProxyMiddlewares := []func(http.Handler) http.Handler{
1573 middleware.PanicRecovery(log),1590 middleware.PanicRecovery(log),
1574 middleware.RequestLogger(log, trustedNets),1591 middleware.RequestLogger(log, trustedNets),
1575- middleware.SecurityHeaders,
1576 }1592 }
1577-
1578- // SECURITY: When proxy_allowed_ips is set, only connections from those CIDRs
1579- // can reach the proxy. This prevents direct-to-origin attacks that bypass
1580- // CDN protections (e.g. Cloudflare WAF, DDoS mitigation).
1581- proxyCIDRMiddleware := buildProxyCIDRAllowlistMiddleware(cfg, trustedNets, log)
1582 if proxyCIDRMiddleware != nil {1593 if proxyCIDRMiddleware != nil {
1583- proxyMiddlewares = append(proxyMiddlewares, proxyCIDRMiddleware)1594+ httpProxyMiddlewares = append(httpProxyMiddlewares, proxyCIDRMiddleware)
1584 }1595 }
1585 1596
1586- proxyHandler := proxyadapter.NewHandler(svc.proxySvc, log)1597+ httpProxyWithMiddleware := otelhttp.NewHandler(
1587- proxyWithMiddleware := otelhttp.NewHandler(1598+ middleware.Chain(httpProxyMiddlewares...)(proxyHandler),
1588- middleware.Chain(proxyMiddlewares...)(proxyHandler),
1589 "gordon.proxy",1599 "gordon.proxy",
1590 )1600 )
1601+
1602+ // Build proxyMux with onboarding routes before catch-all
1591 proxyMux := http.NewServeMux()1603 proxyMux := http.NewServeMux()
1592- proxyMux.Handle("/", proxyWithMiddleware)1604+ if svc.caAdapter != nil {
1605+ onboardingHandler := onboarding.NewHandler(
1606+ svc.caAdapter.RootCertificate(),
1607+ svc.caAdapter.RootCertificateDER(),
1608+ svc.caAdapter.RootCommonName(),
1609+ svc.proxyAllowedNets,
1610+ )
1611+ proxyMux.HandleFunc("GET /ca", onboardingHandler.ServeOnboardingPage)
1612+ proxyMux.HandleFunc("GET /ca.crt", onboardingHandler.ServeCACert)
1613+ proxyMux.HandleFunc("GET /ca.mobileconfig", onboardingHandler.ServeMobileconfig)
1614+ }
1615+ proxyMux.Handle("/", httpProxyWithMiddleware)
1593 1616
1594- registerAdminRoutes(registryMux, svc, cfg, trustedNets, log)1617+ // HTTPS proxy handler chain: security headers + proxy (no CIDR, no onboarding)
1618+ httpsProxyMiddlewares := []func(http.Handler) http.Handler{
1619+ middleware.PanicRecovery(log),
1620+ middleware.RequestLogger(log, trustedNets),
1621+ middleware.SecurityHeaders,
1622+ }
1623+ httpsHandler := otelhttp.NewHandler(
1624+ middleware.Chain(httpsProxyMiddlewares...)(proxyHandler),
1625+ "gordon.proxy.tls",
1626+ )
1595 1627
1596- return registryMux, proxyMux, proxyCIDRMiddleware1628+ return registryMux, proxyMux, httpsHandler
1597 }1629 }
1598 1630
1599 func buildRegistryHandlerWithMiddleware(1631 func buildRegistryHandlerWithMiddleware(
@@ -1677,20 +1709,20 @@ func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet,
1677 return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)1709 return middleware.RegistryCIDRAllowlist(allowedNets, trustedNets, log)
1678 }1710 }
1679 1711
1680-func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler {1712+func buildProxyCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, log zerowrap.Logger) ([]*net.IPNet, func(http.Handler) http.Handler) {
1681 allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.ProxyAllowedIPs, "proxy_allowed_ips", log)1713 allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.ProxyAllowedIPs, "proxy_allowed_ips", log)
1682 if allInvalid {1714 if allInvalid {
1683- return denyAllHandler("proxy_allowed_ips", trustedNets, log)1715+ return nil, denyAllHandler("proxy_allowed_ips", trustedNets, log)
1684 }1716 }
1685 if allowedNets == nil {1717 if allowedNets == nil {
1686- return nil1718+ return nil, nil
1687 }1719 }
1688 1720
1689 log.Info().1721 log.Info().
1690 Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).1722 Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs).
1691 Msg("proxy origin IP allowlist enabled")1723 Msg("proxy origin IP allowlist enabled")
1692 1724
1693- return middleware.ProxyCIDRAllowlist(allowedNets, log)1725+ return allowedNets, middleware.ProxyCIDRAllowlist(allowedNets, log)
1694 }1726 }
1695 1727
1696 func buildRegistryRateLimitMiddleware(cfg Config, log zerowrap.Logger) func(http.Handler) http.Handler {1728 func buildRegistryRateLimitMiddleware(cfg Config, log zerowrap.Logger) func(http.Handler) http.Handler {
@@ -1825,7 +1857,7 @@ func registerAdminRoutes(registryMux *http.ServeMux, svc *services, cfg Config,
1825 // - SIGUSR2: Triggers manual deploy for a specific route1857 // - SIGUSR2: Triggers manual deploy for a specific route
1826 // The deferred signal.Stop calls ensure signal handlers are properly1858 // The deferred signal.Stop calls ensure signal handlers are properly
1827 // cleaned up before program exit, preventing signal handler leaks.1859 // cleaned up before program exit, preventing signal handler leaks.
1828-func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler, proxyHandler http.Handler, proxyCIDRMiddleware func(http.Handler) http.Handler, svc *services, cleanupHandlers func(), log zerowrap.Logger) error {1860+func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services, cleanupHandlers func(), log zerowrap.Logger) error {
1829 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)1861 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
1830 defer cancel()1862 defer cancel()
1831 1863
@@ -1843,15 +1875,17 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
1843 1875
1844 errChan := make(chan error, 3)1876 errChan := make(chan error, 3)
1845 1877
1878+ registryHandler, httpProxyHandler, httpsProxyHandler := createHTTPHandlers(svc, cfg, log)
1879+
1846 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))1880 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))
1847 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)1881 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)
1848 1882
1849- proxySrv, proxyReady, _, _, err := startProxyServers(cfg, proxyHandler, proxyCIDRMiddleware, errChan, log)1883+ proxySrv, proxyReady, tlsSrv, tlsReady, err := startProxyServers(cfg, httpProxyHandler, httpsProxyHandler, svc.pkiSvc, errChan, log)
1850 if err != nil {1884 if err != nil {
1851 return err1885 return err
1852 }1886 }
1853 1887
1854- // Wait for all enabled servers to bind their ports before auto-starting containers.1888+ // Wait for all servers to bind their ports before auto-starting containers.
1855 // This prevents the race where auto-start pulls from the registry before it's listening.1889 // This prevents the race where auto-start pulls from the registry before it's listening.
1856 if err := waitForServerReady(registryReady, errChan); err != nil {1890 if err := waitForServerReady(registryReady, errChan); err != nil {
1857 return err1891 return err
@@ -1859,9 +1893,13 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
1859 if err := waitForServerReady(proxyReady, errChan); err != nil {1893 if err := waitForServerReady(proxyReady, errChan); err != nil {
1860 return err1894 return err
1861 }1895 }
1896+ if err := waitForServerReady(tlsReady, errChan); err != nil {
1897+ return err
1898+ }
1862 1899
1863 log.Info().1900 log.Info().
1864 Int("proxy_port", cfg.Server.Port).1901 Int("proxy_port", cfg.Server.Port).
1902+ Int("tls_port", cfg.Server.TLSPort).
1865 Int("registry_port", cfg.Server.RegistryPort).1903 Int("registry_port", cfg.Server.RegistryPort).
1866 Msg("Gordon is running")1904 Msg("Gordon is running")
1867 1905
@@ -1878,7 +1916,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
1878 1916
1879 waitForShutdown(ctx, errChan, reloadChan, deployChan, svc.eventBus, log)1917 waitForShutdown(ctx, errChan, reloadChan, deployChan, svc.eventBus, log)
1880 cleanupHandlers() // Stop debounce timers before draining containers1918 cleanupHandlers() // Stop debounce timers before draining containers
1881- gracefulShutdown(registrySrv, proxySrv, nil, svc.containerSvc, svc.proxySvc, log)1919+ gracefulShutdown(registrySrv, proxySrv, tlsSrv, svc.containerSvc, svc.proxySvc, svc.pkiSvc, log)
1882 return nil1920 return nil
1883 }1921 }
1884 1922
@@ -2080,7 +2118,7 @@ func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, depl
2080 2118
2081 // gracefulShutdown stops HTTP servers with a 30s timeout, then shuts down2119 // gracefulShutdown stops HTTP servers with a 30s timeout, then shuts down
2082 // the container service and cleans up runtime files.2120 // the container service and cleans up runtime files.
2083-func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, proxySvc *proxy.Service, log zerowrap.Logger) {2121+func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *container.Service, proxySvc *proxy.Service, pkiSvc *pkiusecase.Service, log zerowrap.Logger) {
2084 log.Info().Msg("shutting down Gordon...")2122 log.Info().Msg("shutting down Gordon...")
2085 2123
2086 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)2124 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -2096,6 +2134,11 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
2096 }2134 }
2097 }2135 }
2098 2136
2137+ // Stop PKI maintenance goroutines
2138+ if pkiSvc != nil {
2139+ pkiSvc.Stop()
2140+ }
2141+
2099 // Phase 2: Drain in-flight registry push sessions before stopping the backend2142 // Phase 2: Drain in-flight registry push sessions before stopping the backend
2100 if proxySvc != nil {2143 if proxySvc != nil {
2101 log.Info().Msg("draining in-flight registry requests...")2144 log.Info().Msg("draining in-flight registry requests...")
@@ -2121,14 +2164,38 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
2121 log.Info().Msg("Gordon stopped")2164 log.Info().Msg("Gordon stopped")
2122 }2165 }
2123 2166
2124-// startProxyServers sets up the proxy HTTP server.2167+// startProxyServers sets up the HTTP proxy server and the HTTPS proxy server
2125-// Task 9 will add the TLS server back with PKI-based certificates.2168+// with on-demand TLS certificates from the internal CA.
2126-func startProxyServers(cfg Config, proxyHandler http.Handler, proxyCIDRMiddleware func(http.Handler) http.Handler, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {2169+func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSvc *pkiusecase.Service, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {
2127- var proxyProtos http.Protocols2170+ // HTTP listener (Cloudflare proxy + onboarding for direct clients)
2128- proxyProtos.SetHTTP1(true)2171+ var httpProtos http.Protocols
2129- proxyProtos.SetUnencryptedHTTP2(true)2172+ httpProtos.SetHTTP1(true)
2130- srv, ready := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", &proxyProtos, errChan, log)2173+ httpProtos.SetUnencryptedHTTP2(true)
2131- return srv, ready, nil, nil, nil2174+ httpSrv, httpReady := startServer(
2175+ fmt.Sprintf(":%d", cfg.Server.Port),
2176+ httpHandler,
2177+ "proxy-http",
2178+ &httpProtos,
2179+ errChan,
2180+ log,
2181+ )
2182+
2183+ // HTTPS listener (on-demand TLS via internal CA)
2184+ tlsConfig := &tls.Config{
2185+ MinVersion: tls.VersionTLS12,
2186+ GetCertificate: pkiSvc.GetCertificate,
2187+ NextProtos: []string{"h2", "http/1.1"},
2188+ }
2189+ tlsSrv, tlsReady := startTLSServerWithConfig(
2190+ fmt.Sprintf(":%d", cfg.Server.TLSPort),
2191+ httpsHandler,
2192+ "proxy-tls",
2193+ tlsConfig,
2194+ errChan,
2195+ log,
2196+ )
2197+
2198+ return httpSrv, httpReady, tlsSrv, tlsReady, nil
2132 }2199 }
2133 2200
2134 // startServer starts an HTTP server, returning the server instance and a channel2201 // startServer starts an HTTP server, returning the server instance and a channel
@@ -2168,44 +2235,37 @@ func startServer(addr string, handler http.Handler, name string, protocols *http
2168 return server, ready2235 return server, ready
2169 }2236 }
2170 2237
2171-// startTLSServer starts an HTTPS server with the provided certificate and key.2238+// startTLSServerWithConfig starts an HTTPS server using a pre-built tls.Config
2172-// It returns the server instance (for graceful shutdown) and a channel that closes once the port is bound.2239+// (e.g. with GetCertificate for on-demand issuance via the internal CA).
2173-func startTLSServer(addr string, handler http.Handler, name, certFile, keyFile string, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}) {2240+// Returns the server instance and a channel that closes once the port is bound.
2174- ready := make(chan struct{})2241+func startTLSServerWithConfig(addr string, handler http.Handler, name string, tlsCfg *tls.Config, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}) {
2175-2242+ srv := &http.Server{
2176- server := &http.Server{
2177 Addr: addr,2243 Addr: addr,
2178 Handler: handler,2244 Handler: handler,
2245+ TLSConfig: tlsCfg,
2179 ReadHeaderTimeout: 10 * time.Second,2246 ReadHeaderTimeout: 10 * time.Second,
2180 ReadTimeout: 5 * time.Minute,2247 ReadTimeout: 5 * time.Minute,
2181 WriteTimeout: 5 * time.Minute,2248 WriteTimeout: 5 * time.Minute,
2182 IdleTimeout: 120 * time.Second,2249 IdleTimeout: 120 * time.Second,
2183 MaxHeaderBytes: 1 << 20,2250 MaxHeaderBytes: 1 << 20,
2184- TLSConfig: &tls.Config{
2185- MinVersion: tls.VersionTLS12,
2186- },
2187 }2251 }
2188 2252
2253+ ready := make(chan struct{})
2189 go func() {2254 go func() {
2190- log.Info().
2191- Str("address", addr).
2192- Str("cert_file", certFile).
2193- Str("key_file", keyFile).
2194- Msgf("%s server starting", name)
2195-
2196 ln, err := net.Listen("tcp", addr)2255 ln, err := net.Listen("tcp", addr)
2197 if err != nil {2256 if err != nil {
2198- errChan <- fmt.Errorf("%s server error: %w", name, err)2257+ errChan <- fmt.Errorf("%s listen: %w", name, err)
2199 return2258 return
2200 }2259 }
2260+ tlsLn := tls.NewListener(ln, tlsCfg)
2261+ log.Info().Str("addr", addr).Str("name", name).Msg("TLS server starting")
2201 close(ready)2262 close(ready)
2202-2263+ if err := srv.Serve(tlsLn); err != nil && err != http.ErrServerClosed {
2203- if err := server.ServeTLS(ln, certFile, keyFile); err != nil && err != http.ErrServerClosed {2264+ errChan <- fmt.Errorf("%s: %w", name, err)
2204- errChan <- fmt.Errorf("%s server error: %w", name, err)
2205 }2265 }
2206 }()2266 }()
2207 2267
2208- return server, ready2268+ return srv, ready
2209 }2269 }
2210 2270
2211 // SendReloadSignal sends SIGUSR1 to the running Gordon process.2271 // SendReloadSignal sends SIGUSR1 to the running Gordon process.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close