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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +122 -62
M internal/app/run.go +122 -62
| @@ -32,6 +32,7 @@ import ( | |||
| 32 | 32 | "github.com/bnema/gordon/internal/adapters/out/filesystem" | |
| 33 | 33 | "github.com/bnema/gordon/internal/adapters/out/httpprober" | |
| 34 | 34 | "github.com/bnema/gordon/internal/adapters/out/logwriter" | |
| 35 | + | pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki" | |
| 35 | 36 | "github.com/bnema/gordon/internal/adapters/out/ratelimit" | |
| 36 | 37 | "github.com/bnema/gordon/internal/adapters/out/secrets" | |
| 37 | 38 | "github.com/bnema/gordon/internal/adapters/out/telemetry" | |
| @@ -45,6 +46,7 @@ import ( | |||
| 45 | 46 | "github.com/bnema/gordon/internal/adapters/in/http/admin" | |
| 46 | 47 | authhandler "github.com/bnema/gordon/internal/adapters/in/http/auth" | |
| 47 | 48 | "github.com/bnema/gordon/internal/adapters/in/http/middleware" | |
| 49 | + | "github.com/bnema/gordon/internal/adapters/in/http/onboarding" | |
| 48 | 50 | proxyadapter "github.com/bnema/gordon/internal/adapters/in/http/proxy" | |
| 49 | 51 | "github.com/bnema/gordon/internal/adapters/in/http/registry" | |
| 50 | 52 | ||
| @@ -68,6 +70,7 @@ import ( | |||
| 68 | 70 | "github.com/bnema/gordon/internal/usecase/health" | |
| 69 | 71 | "github.com/bnema/gordon/internal/usecase/images" | |
| 70 | 72 | "github.com/bnema/gordon/internal/usecase/logs" | |
| 73 | + | pkiusecase "github.com/bnema/gordon/internal/usecase/pki" | |
| 71 | 74 | "github.com/bnema/gordon/internal/usecase/proxy" | |
| 72 | 75 | registrySvc "github.com/bnema/gordon/internal/usecase/registry" | |
| 73 | 76 | secretsSvc "github.com/bnema/gordon/internal/usecase/secrets" | |
| @@ -197,6 +200,9 @@ type services struct { | |||
| 197 | 200 | previewService *preview.Service | |
| 198 | 201 | envDir string | |
| 199 | 202 | maxBlobChunkSize int64 | |
| 203 | + | caAdapter *pkiadapter.CA | |
| 204 | + | pkiSvc *pkiusecase.Service | |
| 205 | + | proxyAllowedNets []*net.IPNet // Cloudflare CIDRs for onboarding handler | |
| 200 | 206 | } | |
| 201 | 207 | ||
| 202 | 208 | // Run initializes and starts the Gordon application. | |
| @@ -278,11 +284,8 @@ func Run(ctx context.Context, configPath string) error { | |||
| 278 | 284 | } | |
| 279 | 285 | defer svc.eventBus.Stop() | |
| 280 | 286 | ||
| 281 | - | // Create HTTP handlers | |
| 282 | - | registryHandler, proxyHandler, proxyCIDRMiddleware := createHTTPHandlers(svc, cfg, log) | |
| 283 | - | ||
| 284 | 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 | 291 | // initConfig loads configuration from file. | |
| @@ -373,6 +376,14 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 373 | 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 | 387 | var backend domain.SecretsBackend | |
| 377 | 388 | var passStore *domainsecrets.PassStore | |
| 378 | 389 | var domainSecretStore out.DomainSecretStore | |
| @@ -1545,13 +1556,13 @@ func loopbackOnly(next http.Handler, log zerowrap.Logger) http.Handler { | |||
| 1545 | 1556 | } | |
| 1546 | 1557 | ||
| 1547 | 1558 | // 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) { | |
| 1551 | 1561 | // Parse trusted proxies once for all middleware chains. | |
| 1552 | 1562 | // This ensures consistent IP extraction across logging, rate limiting, and auth. | |
| 1553 | 1563 | trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | |
| 1554 | 1564 | ||
| 1565 | + | // Registry handler (unchanged) | |
| 1555 | 1566 | registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize) | |
| 1556 | 1567 | registryWithMiddleware, cidrAllowlistMiddleware, rateLimitMiddleware := buildRegistryHandlerWithMiddleware( | |
| 1557 | 1568 | svc, | |
| @@ -1564,36 +1575,57 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 1564 | 1575 | registryMux := http.NewServeMux() | |
| 1565 | 1576 | registerAuthRoutes(registryMux, svc, trustedNets, cidrAllowlistMiddleware, rateLimitMiddleware, cfg, log) | |
| 1566 | 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 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{ | |
| 1573 | 1590 | middleware.PanicRecovery(log), | |
| 1574 | 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 | 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) | |
| 1587 | - | proxyWithMiddleware := otelhttp.NewHandler( | |
| 1588 | - | middleware.Chain(proxyMiddlewares...)(proxyHandler), | |
| 1597 | + | httpProxyWithMiddleware := otelhttp.NewHandler( | |
| 1598 | + | middleware.Chain(httpProxyMiddlewares...)(proxyHandler), | |
| 1589 | 1599 | "gordon.proxy", | |
| 1590 | 1600 | ) | |
| 1601 | + | ||
| 1602 | + | // Build proxyMux with onboarding routes before catch-all | |
| 1591 | 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, proxyCIDRMiddleware | |
| 1628 | + | return registryMux, proxyMux, httpsHandler | |
| 1597 | 1629 | } | |
| 1598 | 1630 | ||
| 1599 | 1631 | func buildRegistryHandlerWithMiddleware( | |
| @@ -1677,20 +1709,20 @@ func buildRegistryCIDRAllowlistMiddleware(cfg Config, trustedNets []*net.IPNet, | |||
| 1677 | 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 | 1713 | allowedNets, allInvalid := parseCIDRAllowlist(cfg.Server.ProxyAllowedIPs, "proxy_allowed_ips", log) | |
| 1682 | 1714 | if allInvalid { | |
| 1683 | - | return denyAllHandler("proxy_allowed_ips", trustedNets, log) | |
| 1715 | + | return nil, denyAllHandler("proxy_allowed_ips", trustedNets, log) | |
| 1684 | 1716 | } | |
| 1685 | 1717 | if allowedNets == nil { | |
| 1686 | - | return nil | |
| 1718 | + | return nil, nil | |
| 1687 | 1719 | } | |
| 1688 | 1720 | ||
| 1689 | 1721 | log.Info(). | |
| 1690 | 1722 | Strs("proxy_allowed_ips", cfg.Server.ProxyAllowedIPs). | |
| 1691 | 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 | 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 | 1857 | // - SIGUSR2: Triggers manual deploy for a specific route | |
| 1826 | 1858 | // The deferred signal.Stop calls ensure signal handlers are properly | |
| 1827 | 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 | 1861 | ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) | |
| 1830 | 1862 | defer cancel() | |
| 1831 | 1863 | ||
| @@ -1843,15 +1875,17 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler | |||
| 1843 | 1875 | ||
| 1844 | 1876 | errChan := make(chan error, 3) | |
| 1845 | 1877 | ||
| 1878 | + | registryHandler, httpProxyHandler, httpsProxyHandler := createHTTPHandlers(svc, cfg, log) | |
| 1879 | + | ||
| 1846 | 1880 | registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort)) | |
| 1847 | 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 | 1884 | if err != nil { | |
| 1851 | 1885 | 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 | 1889 | // This prevents the race where auto-start pulls from the registry before it's listening. | |
| 1856 | 1890 | if err := waitForServerReady(registryReady, errChan); err != nil { | |
| 1857 | 1891 | return err | |
| @@ -1859,9 +1893,13 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler | |||
| 1859 | 1893 | if err := waitForServerReady(proxyReady, errChan); err != nil { | |
| 1860 | 1894 | return err | |
| 1861 | 1895 | } | |
| 1896 | + | if err := waitForServerReady(tlsReady, errChan); err != nil { | |
| 1897 | + | return err | |
| 1898 | + | } | |
| 1862 | 1899 | ||
| 1863 | 1900 | log.Info(). | |
| 1864 | 1901 | Int("proxy_port", cfg.Server.Port). | |
| 1902 | + | Int("tls_port", cfg.Server.TLSPort). | |
| 1865 | 1903 | Int("registry_port", cfg.Server.RegistryPort). | |
| 1866 | 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 | 1917 | waitForShutdown(ctx, errChan, reloadChan, deployChan, svc.eventBus, log) | |
| 1880 | 1918 | 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 | 1920 | return nil | |
| 1883 | 1921 | } | |
| 1884 | 1922 | ||
| @@ -2080,7 +2118,7 @@ func waitForShutdown(ctx context.Context, errChan <-chan error, reloadChan, depl | |||
| 2080 | 2118 | ||
| 2081 | 2119 | // gracefulShutdown stops HTTP servers with a 30s timeout, then shuts down | |
| 2082 | 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 | 2122 | log.Info().Msg("shutting down Gordon...") | |
| 2085 | 2123 | ||
| 2086 | 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 | 2142 | // Phase 2: Drain in-flight registry push sessions before stopping the backend | |
| 2100 | 2143 | if proxySvc != nil { | |
| 2101 | 2144 | log.Info().Msg("draining in-flight registry requests...") | |
| @@ -2121,14 +2164,38 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc * | |||
| 2121 | 2164 | log.Info().Msg("Gordon stopped") | |
| 2122 | 2165 | } | |
| 2123 | 2166 | ||
| 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 | |
| 2132 | 2199 | } | |
| 2133 | 2200 | ||
| 2134 | 2201 | // 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 | 2235 | return server, ready | |
| 2169 | 2236 | } | |
| 2170 | 2237 | ||
| 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{ | |
| 2177 | 2243 | Addr: addr, | |
| 2178 | 2244 | Handler: handler, | |
| 2245 | + | TLSConfig: tlsCfg, | |
| 2179 | 2246 | ReadHeaderTimeout: 10 * time.Second, | |
| 2180 | 2247 | ReadTimeout: 5 * time.Minute, | |
| 2181 | 2248 | WriteTimeout: 5 * time.Minute, | |
| 2182 | 2249 | IdleTimeout: 120 * time.Second, | |
| 2183 | 2250 | MaxHeaderBytes: 1 << 20, | |
| 2184 | - | TLSConfig: &tls.Config{ | |
| 2185 | - | MinVersion: tls.VersionTLS12, | |
| 2186 | - | }, | |
| 2187 | 2251 | } | |
| 2188 | 2252 | ||
| 2253 | + | ready := make(chan struct{}) | |
| 2189 | 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 | 2255 | ln, err := net.Listen("tcp", addr) | |
| 2197 | 2256 | if err != nil { | |
| 2198 | - | errChan <- fmt.Errorf("%s server error: %w", name, err) | |
| 2257 | + | errChan <- fmt.Errorf("%s listen: %w", name, err) | |
| 2199 | 2258 | return | |
| 2200 | 2259 | } | |
| 2260 | + | tlsLn := tls.NewListener(ln, tlsCfg) | |
| 2261 | + | log.Info().Str("addr", addr).Str("name", name).Msg("TLS server starting") | |
| 2201 | 2262 | 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) | |
| 2205 | 2265 | } | |
| 2206 | 2266 | }() | |
| 2207 | 2267 | ||
| 2208 | - | return server, ready | |
| 2268 | + | return srv, ready | |
| 2209 | 2269 | } | |
| 2210 | 2270 | ||
| 2211 | 2271 | // 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.Service | 200 | previewService *preview.Service | ||
| 198 | envDir string | 201 | envDir string | ||
| 199 | maxBlobChunkSize int64 | 202 | 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.SecretsBackend | 387 | var backend domain.SecretsBackend | ||
| 377 | var passStore *domainsecrets.PassStore | 388 | var passStore *domainsecrets.PassStore | ||
| 378 | var domainSecretStore out.DomainSecretStore | 389 | 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 applications | 1580 | + | // 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 make | 1582 | + | |
| 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, proxyCIDRMiddleware | 1628 | + | 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 nil | 1718 | + | 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 route | 1857 | // - SIGUSR2: Triggers manual deploy for a specific route | ||
| 1826 | // The deferred signal.Stop calls ensure signal handlers are properly | 1858 | // 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 err | 1885 | 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 err | 1891 | 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 err | 1894 | 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 containers | 1918 | 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 nil | 1920 | 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 down | 2119 | // 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 backend | 2142 | // 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.Protocols | 2170 | + | // 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, nil | 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 | |||
| 2132 | } | 2199 | } | ||
| 2133 | 2200 | ||||
| 2134 | // startServer starts an HTTP server, returning the server instance and a channel | 2201 | // 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, ready | 2235 | 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 | return | 2258 | 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, ready | 2268 | + | 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. | ||