detached 1659415

refactor: remove deprecated TLS config fields, forceHSTS, and self-signed cert generation

commit
16594159cbfa0be125b32dc10b5c1cbdf2d8ea45
author
brice · · 2026-03-29 16:53 UTC
parent
848492c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +32 -243
M internal/adapters/in/http/middleware/security.go +14 -31
@@ -1,38 +1,21 @@
11 package middleware
22
3-import (
4- "net/http"
5-)
3+import "net/http"
64
7-// SecurityHeaders middleware adds standard security headers to HTTP responses.
5+// SecurityHeaders adds standard security headers to HTTP responses.
6+// HSTS is set automatically when the connection uses TLS (r.TLS != nil).
87 func SecurityHeaders(next http.Handler) http.Handler {
9- return SecurityHeadersWithOptions(false)(next)
10-}
11-
12-// SecurityHeadersWithOptions returns a middleware that adds standard security headers.
13-// When forceHSTS is true, the HSTS header is always set regardless of TLS state.
14-// This is needed when Gordon runs behind a TLS-terminating proxy (e.g. Cloudflare)
15-// where r.TLS is nil but the client connection is always HTTPS.
16-func SecurityHeadersWithOptions(forceHSTS bool) func(http.Handler) http.Handler {
17- return func(next http.Handler) http.Handler {
18- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19- w.Header().Set("X-Content-Type-Options", "nosniff")
20- w.Header().Set("X-Frame-Options", "DENY")
21- w.Header().Set("X-XSS-Protection", "1; mode=block")
22- w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
23- w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
24-
25- // Only set when TLS is active or force_hsts is configured,
26- // since HSTS over plain HTTP would lock out clients.
27- if r.TLS != nil || forceHSTS {
28- w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
29- }
8+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
9+ w.Header().Set("X-Content-Type-Options", "nosniff")
10+ w.Header().Set("X-Frame-Options", "DENY")
11+ w.Header().Set("X-XSS-Protection", "1; mode=block")
12+ w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
13+ w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
3014
31- // CSP is intentionally NOT set here — Gordon proxies arbitrary web apps,
32- // so a blanket policy would break proxied sites. CSP is set only on
33- // Gordon-generated error responses (see proxy error handlers).
15+ if r.TLS != nil {
16+ w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
17+ }
3418
35- next.ServeHTTP(w, r)
36- })
37- }
19+ next.ServeHTTP(w, r)
20+ })
3821 }
@@ -1,38 +1,21 @@
1 package middleware1 package middleware
2 2
3-import (3+import "net/http"
4- "net/http"
5-)
6 4
7-// SecurityHeaders middleware adds standard security headers to HTTP responses.5+// SecurityHeaders adds standard security headers to HTTP responses.
6+// HSTS is set automatically when the connection uses TLS (r.TLS != nil).
8 func SecurityHeaders(next http.Handler) http.Handler {7 func SecurityHeaders(next http.Handler) http.Handler {
9- return SecurityHeadersWithOptions(false)(next)8+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
10-}9+ w.Header().Set("X-Content-Type-Options", "nosniff")
11-10+ w.Header().Set("X-Frame-Options", "DENY")
12-// SecurityHeadersWithOptions returns a middleware that adds standard security headers.11+ w.Header().Set("X-XSS-Protection", "1; mode=block")
13-// When forceHSTS is true, the HSTS header is always set regardless of TLS state.12+ w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
14-// This is needed when Gordon runs behind a TLS-terminating proxy (e.g. Cloudflare)13+ w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
15-// where r.TLS is nil but the client connection is always HTTPS.
16-func SecurityHeadersWithOptions(forceHSTS bool) func(http.Handler) http.Handler {
17- return func(next http.Handler) http.Handler {
18- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19- w.Header().Set("X-Content-Type-Options", "nosniff")
20- w.Header().Set("X-Frame-Options", "DENY")
21- w.Header().Set("X-XSS-Protection", "1; mode=block")
22- w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
23- w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
24-
25- // Only set when TLS is active or force_hsts is configured,
26- // since HSTS over plain HTTP would lock out clients.
27- if r.TLS != nil || forceHSTS {
28- w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
29- }
30 14
31- // CSP is intentionally NOT set here — Gordon proxies arbitrary web apps,15+ if r.TLS != nil {
32- // so a blanket policy would break proxied sites. CSP is set only on16+ w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
33- // Gordon-generated error responses (see proxy error handlers).17+ }
34 18
35- next.ServeHTTP(w, r)19+ next.ServeHTTP(w, r)
36- })20+ })
37- }
38 }21 }
M internal/adapters/in/http/middleware/security_test.go +0 -16
@@ -51,22 +51,6 @@ func TestSecurityHeaders_HSTS_WithTLS(t *testing.T) {
5151 assert.Equal(t, "max-age=31536000; includeSubDomains", rec.Header().Get("Strict-Transport-Security"))
5252 }
5353
54-func TestSecurityHeaders_HSTS_ForceWithoutTLS(t *testing.T) {
55- handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
56- w.WriteHeader(http.StatusOK)
57- })
58-
59- wrapped := SecurityHeadersWithOptions(true)(handler)
60-
61- req := httptest.NewRequest(http.MethodGet, "/", nil)
62- // r.TLS is nil — no TLS on the connection, but forceHSTS is true
63- rec := httptest.NewRecorder()
64- wrapped.ServeHTTP(rec, req)
65-
66- assert.Equal(t, "max-age=31536000; includeSubDomains", rec.Header().Get("Strict-Transport-Security"),
67- "HSTS should be set when forceHSTS is true even without TLS")
68-}
69-
7054 func TestSecurityHeaders_HSTS_WithForwardedProto_NotTrusted(t *testing.T) {
7155 handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
7256 w.WriteHeader(http.StatusOK)
@@ -51,22 +51,6 @@ func TestSecurityHeaders_HSTS_WithTLS(t *testing.T) {
51 assert.Equal(t, "max-age=31536000; includeSubDomains", rec.Header().Get("Strict-Transport-Security"))51 assert.Equal(t, "max-age=31536000; includeSubDomains", rec.Header().Get("Strict-Transport-Security"))
52 }52 }
53 53
54-func TestSecurityHeaders_HSTS_ForceWithoutTLS(t *testing.T) {
55- handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
56- w.WriteHeader(http.StatusOK)
57- })
58-
59- wrapped := SecurityHeadersWithOptions(true)(handler)
60-
61- req := httptest.NewRequest(http.MethodGet, "/", nil)
62- // r.TLS is nil — no TLS on the connection, but forceHSTS is true
63- rec := httptest.NewRecorder()
64- wrapped.ServeHTTP(rec, req)
65-
66- assert.Equal(t, "max-age=31536000; includeSubDomains", rec.Header().Get("Strict-Transport-Security"),
67- "HSTS should be set when forceHSTS is true even without TLS")
68-}
69-
70 func TestSecurityHeaders_HSTS_WithForwardedProto_NotTrusted(t *testing.T) {54 func TestSecurityHeaders_HSTS_WithForwardedProto_NotTrusted(t *testing.T) {
71 handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {55 handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
72 w.WriteHeader(http.StatusOK)56 w.WriteHeader(http.StatusOK)
M internal/app/run.go +18 -196
@@ -3,18 +3,12 @@ package app
33
44 import (
55 "context"
6- "crypto/ecdsa"
7- "crypto/elliptic"
86 "crypto/rand"
97 "crypto/tls"
10- "crypto/x509"
11- "crypto/x509/pkix"
128 "encoding/hex"
139 "encoding/json"
14- "encoding/pem"
1510 "errors"
1611 "fmt"
17- "math/big"
1812 "net"
1913 "net/http"
2014 "os"
@@ -91,10 +85,7 @@ type Config struct {
9185 RegistryPort int `mapstructure:"registry_port"`
9286 GordonDomain string `mapstructure:"gordon_domain"`
9387 RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain
94- TLSEnabled bool `mapstructure:"tls_enabled"`
9588 TLSPort int `mapstructure:"tls_port"`
96- TLSCertFile string `mapstructure:"tls_cert_file"`
97- TLSKeyFile string `mapstructure:"tls_key_file"`
9889 DataDir string `mapstructure:"data_dir"`
9990 MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB"
10091 MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB"
@@ -103,7 +94,6 @@ type Config struct {
10394 RegistryAllowedIPs []string `mapstructure:"registry_allowed_ips"`
10495 ProxyAllowedIPs []string `mapstructure:"proxy_allowed_ips"`
10596 RegistryListenAddr string `mapstructure:"registry_listen_address"`
106- ForceHSTS bool `mapstructure:"force_hsts"`
10797 } `mapstructure:"server"`
10898
10999 Logging struct {
@@ -252,8 +242,11 @@ func Run(ctx context.Context, configPath string) error {
252242
253243 log.Info().Msg("Gordon starting")
254244
255- if err := ensureTLSConfig(&cfg, log); err != nil {
256- return err
245+ // Deprecation warnings for removed TLS config fields
246+ for _, key := range []string{"server.tls_enabled", "server.tls_cert_file", "server.tls_key_file", "server.force_hsts"} {
247+ if v.IsSet(key) {
248+ log.Warn().Str("key", key).Msg("deprecated config key — Gordon now uses an internal CA with automatic TLS; remove this from your config")
249+ }
257250 }
258251
259252 // Create PID file
@@ -292,123 +285,6 @@ func Run(ctx context.Context, configPath string) error {
292285 return runServers(ctx, v, cfg, registryHandler, proxyHandler, proxyCIDRMiddleware, svc, cleanupHandlers, log)
293286 }
294287
295-func ensureTLSConfig(cfg *Config, log zerowrap.Logger) error {
296- if !cfg.Server.TLSEnabled {
297- return nil
298- }
299-
300- if cfg.Server.TLSCertFile != "" && cfg.Server.TLSKeyFile != "" {
301- return nil
302- }
303-
304- dataDir := resolveDataDir(cfg.Server.DataDir)
305- tlsDir := filepath.Join(dataDir, "tls")
306- certPath := filepath.Join(tlsDir, "cert.pem")
307- keyPath := filepath.Join(tlsDir, "key.pem")
308-
309- domainName := cfg.Server.GordonDomain
310- if domainName == "" {
311- domainName = cfg.Server.RegistryDomain
312- }
313- if domainName == "" {
314- domainName = "localhost"
315- }
316-
317- if err := generateSelfSignedTLSCert(certPath, keyPath, domainName); err != nil {
318- return fmt.Errorf("failed to generate self-signed TLS certificate: %w", err)
319- }
320-
321- cfg.Server.TLSCertFile = certPath
322- cfg.Server.TLSKeyFile = keyPath
323-
324- log.Warn().
325- Str("cert_file", certPath).
326- Str("key_file", keyPath).
327- Str("domain", domainName).
328- Msg("server.tls_enabled=true with empty tls_cert_file/tls_key_file; using auto-generated self-signed certificate")
329-
330- return nil
331-}
332-
333-func generateSelfSignedTLSCert(certPath, keyPath, domainName string) error {
334- // Reuse existing pair if already generated.
335- if _, err := os.Stat(certPath); err == nil {
336- if _, keyErr := os.Stat(keyPath); keyErr == nil {
337- return nil
338- }
339- }
340-
341- if err := os.MkdirAll(filepath.Dir(certPath), 0700); err != nil {
342- return err
343- }
344- if err := os.MkdirAll(filepath.Dir(keyPath), 0700); err != nil {
345- return err
346- }
347-
348- priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
349- if err != nil {
350- return err
351- }
352-
353- now := time.Now()
354- serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
355- serialNumber, err := rand.Int(rand.Reader, serialLimit)
356- if err != nil {
357- return err
358- }
359-
360- template := x509.Certificate{
361- SerialNumber: serialNumber,
362- Subject: pkix.Name{
363- CommonName: domainName,
364- Organization: []string{"Gordon Auto-Generated TLS"},
365- },
366- NotBefore: now.Add(-5 * time.Minute),
367- NotAfter: now.AddDate(1, 0, 0),
368- KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
369- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
370- BasicConstraintsValid: true,
371- }
372-
373- if ip := net.ParseIP(domainName); ip != nil {
374- template.IPAddresses = []net.IP{ip}
375- } else {
376- template.DNSNames = []string{domainName}
377- }
378-
379- derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
380- if err != nil {
381- return err
382- }
383-
384- certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
385- if err != nil {
386- return err
387- }
388- defer certOut.Close()
389-
390- if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
391- return err
392- }
393-
394- keyBytes, err := x509.MarshalECPrivateKey(priv)
395- if err != nil {
396- return err
397- }
398-
399- keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
400- if err != nil {
401- return err
402- }
403- defer keyOut.Close()
404-
405- if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}); err != nil {
406- return err
407- }
408-
409- return nil
410-}
411-
412288 // initConfig loads configuration from file.
413289 func initConfig(configPath string) (*viper.Viper, Config, error) {
414290 v := viper.New()
@@ -1696,7 +1572,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
16961572 proxyMiddlewares := []func(http.Handler) http.Handler{
16971573 middleware.PanicRecovery(log),
16981574 middleware.RequestLogger(log, trustedNets),
1699- middleware.SecurityHeadersWithOptions(cfg.Server.ForceHSTS),
1575+ middleware.SecurityHeaders,
17001576 }
17011577
17021578 // SECURITY: When proxy_allowed_ips is set, only connections from those CIDRs
@@ -1970,7 +1846,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
19701846 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))
19711847 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)
19721848
1973- proxySrv, proxyReady, tlsSrv, tlsReady, err := startProxyServers(cfg, proxyHandler, proxyCIDRMiddleware, errChan, log)
1849+ proxySrv, proxyReady, _, _, err := startProxyServers(cfg, proxyHandler, proxyCIDRMiddleware, errChan, log)
19741850 if err != nil {
19751851 return err
19761852 }
@@ -1983,19 +1859,11 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
19831859 if err := waitForServerReady(proxyReady, errChan); err != nil {
19841860 return err
19851861 }
1986- if cfg.Server.TLSEnabled {
1987- if err := waitForServerReady(tlsReady, errChan); err != nil {
1988- return err
1989- }
1990- }
19911862
1992- logEvent := log.Info().
1863+ log.Info().
19931864 Int("proxy_port", cfg.Server.Port).
1994- Int("registry_port", cfg.Server.RegistryPort)
1995- if cfg.Server.TLSEnabled {
1996- logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort)
1997- }
1998- logEvent.Msg("Gordon is running")
1865+ Int("registry_port", cfg.Server.RegistryPort).
1866+ Msg("Gordon is running")
19991867
20001868 schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v)
20011869 if err != nil {
@@ -2010,7 +1878,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
20101878
20111879 waitForShutdown(ctx, errChan, reloadChan, deployChan, svc.eventBus, log)
20121880 cleanupHandlers() // Stop debounce timers before draining containers
2013- gracefulShutdown(registrySrv, proxySrv, tlsSrv, svc.containerSvc, svc.proxySvc, log)
1881+ gracefulShutdown(registrySrv, proxySrv, nil, svc.containerSvc, svc.proxySvc, log)
20141882 return nil
20151883 }
20161884
@@ -2253,56 +2121,14 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
22532121 log.Info().Msg("Gordon stopped")
22542122 }
22552123
2256-// startProxyServers sets up proxy server(s) depending on TLS configuration.
2257-// With TLS enabled, the HTTP port becomes a redirect-only server and a separate TLS server handles traffic.
2258-// Without TLS, a single server handles both HTTP/1.1 and cleartext HTTP/2.
2259-// proxyCIDRMiddleware, when non-nil, is also applied to the HTTP redirect listener
2260-// so that proxy_allowed_ips cannot be bypassed via the plain-HTTP port.
2124+// startProxyServers sets up the proxy HTTP server.
2125+// Task 9 will add the TLS server back with PKI-based certificates.
22612126 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) {
2262- if !cfg.Server.TLSEnabled {
2263- var proxyProtos http.Protocols
2264- proxyProtos.SetHTTP1(true)
2265- proxyProtos.SetUnencryptedHTTP2(true)
2266- srv, ready := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", &proxyProtos, errChan, log)
2267- return srv, ready, nil, nil, nil
2268- }
2269-
2270- if cfg.Server.TLSCertFile == "" || cfg.Server.TLSKeyFile == "" {
2271- return nil, nil, nil, nil, fmt.Errorf("server.tls_enabled=true requires both server.tls_cert_file and server.tls_key_file")
2272- }
2273-
2274- tlsPort := cfg.Server.TLSPort
2275- redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2276- host := r.Host
2277- if h, _, err := net.SplitHostPort(host); err == nil {
2278- host = h
2279- } else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
2280- host = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]")
2281- }
2282- if tlsPort != 443 {
2283- host = net.JoinHostPort(host, strconv.Itoa(tlsPort))
2284- } else if strings.Contains(host, ":") {
2285- host = "[" + host + "]"
2286- }
2287- target := "https://" + host + r.RequestURI
2288- http.Redirect(w, r, target, http.StatusPermanentRedirect)
2289- })
2290-
2291- var redirectWithMiddleware http.Handler = redirectHandler
2292- if proxyCIDRMiddleware != nil {
2293- redirectWithMiddleware = proxyCIDRMiddleware(redirectHandler)
2294- }
2295- proxySrv, proxyReady := startServer(fmt.Sprintf(":%d", cfg.Server.Port), redirectWithMiddleware, "proxy-redirect", nil, errChan, log)
2296- tlsSrv, tlsReady := startTLSServer(
2297- fmt.Sprintf(":%d", cfg.Server.TLSPort),
2298- proxyHandler,
2299- "proxy-tls",
2300- cfg.Server.TLSCertFile,
2301- cfg.Server.TLSKeyFile,
2302- errChan,
2303- log,
2304- )
2305- return proxySrv, proxyReady, tlsSrv, tlsReady, nil
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
23062132 }
23072133
23082134 // startServer starts an HTTP server, returning the server instance and a channel
@@ -2635,10 +2461,7 @@ func isProcessAlive(pid int) bool {
26352461 func loadConfig(v *viper.Viper, configPath string) error {
26362462 v.SetDefault("server.port", 80)
26372463 v.SetDefault("server.registry_port", 5000)
2638- v.SetDefault("server.tls_enabled", false)
26392464 v.SetDefault("server.tls_port", 443)
2640- v.SetDefault("server.tls_cert_file", "")
2641- v.SetDefault("server.tls_key_file", "")
26422465 v.SetDefault("server.data_dir", DefaultDataDir())
26432466 v.SetDefault("server.runtime", "auto")
26442467 v.SetDefault("logging.level", "info")
@@ -2690,7 +2513,6 @@ func loadConfig(v *viper.Viper, configPath string) error {
26902513 v.SetDefault("server.registry_allowed_ips", []string{})
26912514 v.SetDefault("server.proxy_allowed_ips", []string{})
26922515 v.SetDefault("server.registry_listen_address", "")
2693- v.SetDefault("server.force_hsts", false)
26942516 v.SetDefault("deploy.readiness_delay", "5s")
26952517 v.SetDefault("deploy.readiness_mode", "auto")
26962518 v.SetDefault("deploy.health_timeout", "90s")
@@ -3,18 +3,12 @@ package app
3 3
4 import (4 import (
5 "context"5 "context"
6- "crypto/ecdsa"
7- "crypto/elliptic"
8 "crypto/rand"6 "crypto/rand"
9 "crypto/tls"7 "crypto/tls"
10- "crypto/x509"
11- "crypto/x509/pkix"
12 "encoding/hex"8 "encoding/hex"
13 "encoding/json"9 "encoding/json"
14- "encoding/pem"
15 "errors"10 "errors"
16 "fmt"11 "fmt"
17- "math/big"
18 "net"12 "net"
19 "net/http"13 "net/http"
20 "os"14 "os"
@@ -91,10 +85,7 @@ type Config struct {
91 RegistryPort int `mapstructure:"registry_port"`85 RegistryPort int `mapstructure:"registry_port"`
92 GordonDomain string `mapstructure:"gordon_domain"`86 GordonDomain string `mapstructure:"gordon_domain"`
93 RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain87 RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain
94- TLSEnabled bool `mapstructure:"tls_enabled"`
95 TLSPort int `mapstructure:"tls_port"`88 TLSPort int `mapstructure:"tls_port"`
96- TLSCertFile string `mapstructure:"tls_cert_file"`
97- TLSKeyFile string `mapstructure:"tls_key_file"`
98 DataDir string `mapstructure:"data_dir"`89 DataDir string `mapstructure:"data_dir"`
99 MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB"90 MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB"
100 MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB"91 MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB"
@@ -103,7 +94,6 @@ type Config struct {
103 RegistryAllowedIPs []string `mapstructure:"registry_allowed_ips"`94 RegistryAllowedIPs []string `mapstructure:"registry_allowed_ips"`
104 ProxyAllowedIPs []string `mapstructure:"proxy_allowed_ips"`95 ProxyAllowedIPs []string `mapstructure:"proxy_allowed_ips"`
105 RegistryListenAddr string `mapstructure:"registry_listen_address"`96 RegistryListenAddr string `mapstructure:"registry_listen_address"`
106- ForceHSTS bool `mapstructure:"force_hsts"`
107 } `mapstructure:"server"`97 } `mapstructure:"server"`
108 98
109 Logging struct {99 Logging struct {
@@ -252,8 +242,11 @@ func Run(ctx context.Context, configPath string) error {
252 242
253 log.Info().Msg("Gordon starting")243 log.Info().Msg("Gordon starting")
254 244
255- if err := ensureTLSConfig(&cfg, log); err != nil {245+ // Deprecation warnings for removed TLS config fields
256- return err246+ for _, key := range []string{"server.tls_enabled", "server.tls_cert_file", "server.tls_key_file", "server.force_hsts"} {
247+ if v.IsSet(key) {
248+ log.Warn().Str("key", key).Msg("deprecated config key — Gordon now uses an internal CA with automatic TLS; remove this from your config")
249+ }
257 }250 }
258 251
259 // Create PID file252 // Create PID file
@@ -292,123 +285,6 @@ func Run(ctx context.Context, configPath string) error {
292 return runServers(ctx, v, cfg, registryHandler, proxyHandler, proxyCIDRMiddleware, svc, cleanupHandlers, log)285 return runServers(ctx, v, cfg, registryHandler, proxyHandler, proxyCIDRMiddleware, svc, cleanupHandlers, log)
293 }286 }
294 287
295-func ensureTLSConfig(cfg *Config, log zerowrap.Logger) error {
296- if !cfg.Server.TLSEnabled {
297- return nil
298- }
299-
300- if cfg.Server.TLSCertFile != "" && cfg.Server.TLSKeyFile != "" {
301- return nil
302- }
303-
304- dataDir := resolveDataDir(cfg.Server.DataDir)
305- tlsDir := filepath.Join(dataDir, "tls")
306- certPath := filepath.Join(tlsDir, "cert.pem")
307- keyPath := filepath.Join(tlsDir, "key.pem")
308-
309- domainName := cfg.Server.GordonDomain
310- if domainName == "" {
311- domainName = cfg.Server.RegistryDomain
312- }
313- if domainName == "" {
314- domainName = "localhost"
315- }
316-
317- if err := generateSelfSignedTLSCert(certPath, keyPath, domainName); err != nil {
318- return fmt.Errorf("failed to generate self-signed TLS certificate: %w", err)
319- }
320-
321- cfg.Server.TLSCertFile = certPath
322- cfg.Server.TLSKeyFile = keyPath
323-
324- log.Warn().
325- Str("cert_file", certPath).
326- Str("key_file", keyPath).
327- Str("domain", domainName).
328- Msg("server.tls_enabled=true with empty tls_cert_file/tls_key_file; using auto-generated self-signed certificate")
329-
330- return nil
331-}
332-
333-func generateSelfSignedTLSCert(certPath, keyPath, domainName string) error {
334- // Reuse existing pair if already generated.
335- if _, err := os.Stat(certPath); err == nil {
336- if _, keyErr := os.Stat(keyPath); keyErr == nil {
337- return nil
338- }
339- }
340-
341- if err := os.MkdirAll(filepath.Dir(certPath), 0700); err != nil {
342- return err
343- }
344- if err := os.MkdirAll(filepath.Dir(keyPath), 0700); err != nil {
345- return err
346- }
347-
348- priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
349- if err != nil {
350- return err
351- }
352-
353- now := time.Now()
354- serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
355- serialNumber, err := rand.Int(rand.Reader, serialLimit)
356- if err != nil {
357- return err
358- }
359-
360- template := x509.Certificate{
361- SerialNumber: serialNumber,
362- Subject: pkix.Name{
363- CommonName: domainName,
364- Organization: []string{"Gordon Auto-Generated TLS"},
365- },
366- NotBefore: now.Add(-5 * time.Minute),
367- NotAfter: now.AddDate(1, 0, 0),
368- KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
369- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
370- BasicConstraintsValid: true,
371- }
372-
373- if ip := net.ParseIP(domainName); ip != nil {
374- template.IPAddresses = []net.IP{ip}
375- } else {
376- template.DNSNames = []string{domainName}
377- }
378-
379- derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
380- if err != nil {
381- return err
382- }
383-
384- certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
385- if err != nil {
386- return err
387- }
388- defer certOut.Close()
389-
390- if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
391- return err
392- }
393-
394- keyBytes, err := x509.MarshalECPrivateKey(priv)
395- if err != nil {
396- return err
397- }
398-
399- keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
400- if err != nil {
401- return err
402- }
403- defer keyOut.Close()
404-
405- if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}); err != nil {
406- return err
407- }
408-
409- return nil
410-}
411-
412 // initConfig loads configuration from file.288 // initConfig loads configuration from file.
413 func initConfig(configPath string) (*viper.Viper, Config, error) {289 func initConfig(configPath string) (*viper.Viper, Config, error) {
414 v := viper.New()290 v := viper.New()
@@ -1696,7 +1572,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
1696 proxyMiddlewares := []func(http.Handler) http.Handler{1572 proxyMiddlewares := []func(http.Handler) http.Handler{
1697 middleware.PanicRecovery(log),1573 middleware.PanicRecovery(log),
1698 middleware.RequestLogger(log, trustedNets),1574 middleware.RequestLogger(log, trustedNets),
1699- middleware.SecurityHeadersWithOptions(cfg.Server.ForceHSTS),1575+ middleware.SecurityHeaders,
1700 }1576 }
1701 1577
1702 // SECURITY: When proxy_allowed_ips is set, only connections from those CIDRs1578 // SECURITY: When proxy_allowed_ips is set, only connections from those CIDRs
@@ -1970,7 +1846,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
1970 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))1846 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))
1971 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)1847 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)
1972 1848
1973- proxySrv, proxyReady, tlsSrv, tlsReady, err := startProxyServers(cfg, proxyHandler, proxyCIDRMiddleware, errChan, log)1849+ proxySrv, proxyReady, _, _, err := startProxyServers(cfg, proxyHandler, proxyCIDRMiddleware, errChan, log)
1974 if err != nil {1850 if err != nil {
1975 return err1851 return err
1976 }1852 }
@@ -1983,19 +1859,11 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
1983 if err := waitForServerReady(proxyReady, errChan); err != nil {1859 if err := waitForServerReady(proxyReady, errChan); err != nil {
1984 return err1860 return err
1985 }1861 }
1986- if cfg.Server.TLSEnabled {
1987- if err := waitForServerReady(tlsReady, errChan); err != nil {
1988- return err
1989- }
1990- }
1991 1862
1992- logEvent := log.Info().1863+ log.Info().
1993 Int("proxy_port", cfg.Server.Port).1864 Int("proxy_port", cfg.Server.Port).
1994- Int("registry_port", cfg.Server.RegistryPort)1865+ Int("registry_port", cfg.Server.RegistryPort).
1995- if cfg.Server.TLSEnabled {1866+ Msg("Gordon is running")
1996- logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort)
1997- }
1998- logEvent.Msg("Gordon is running")
1999 1867
2000 schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v)1868 schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v)
2001 if err != nil {1869 if err != nil {
@@ -2010,7 +1878,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
2010 1878
2011 waitForShutdown(ctx, errChan, reloadChan, deployChan, svc.eventBus, log)1879 waitForShutdown(ctx, errChan, reloadChan, deployChan, svc.eventBus, log)
2012 cleanupHandlers() // Stop debounce timers before draining containers1880 cleanupHandlers() // Stop debounce timers before draining containers
2013- gracefulShutdown(registrySrv, proxySrv, tlsSrv, svc.containerSvc, svc.proxySvc, log)1881+ gracefulShutdown(registrySrv, proxySrv, nil, svc.containerSvc, svc.proxySvc, log)
2014 return nil1882 return nil
2015 }1883 }
2016 1884
@@ -2253,56 +2121,14 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
2253 log.Info().Msg("Gordon stopped")2121 log.Info().Msg("Gordon stopped")
2254 }2122 }
2255 2123
2256-// startProxyServers sets up proxy server(s) depending on TLS configuration.2124+// startProxyServers sets up the proxy HTTP server.
2257-// With TLS enabled, the HTTP port becomes a redirect-only server and a separate TLS server handles traffic.2125+// Task 9 will add the TLS server back with PKI-based certificates.
2258-// Without TLS, a single server handles both HTTP/1.1 and cleartext HTTP/2.
2259-// proxyCIDRMiddleware, when non-nil, is also applied to the HTTP redirect listener
2260-// so that proxy_allowed_ips cannot be bypassed via the plain-HTTP port.
2261 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) {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) {
2262- if !cfg.Server.TLSEnabled {2127+ var proxyProtos http.Protocols
2263- var proxyProtos http.Protocols2128+ proxyProtos.SetHTTP1(true)
2264- proxyProtos.SetHTTP1(true)2129+ proxyProtos.SetUnencryptedHTTP2(true)
2265- proxyProtos.SetUnencryptedHTTP2(true)2130+ srv, ready := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", &proxyProtos, errChan, log)
2266- srv, ready := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", &proxyProtos, errChan, log)2131+ return srv, ready, nil, nil, nil
2267- return srv, ready, nil, nil, nil
2268- }
2269-
2270- if cfg.Server.TLSCertFile == "" || cfg.Server.TLSKeyFile == "" {
2271- return nil, nil, nil, nil, fmt.Errorf("server.tls_enabled=true requires both server.tls_cert_file and server.tls_key_file")
2272- }
2273-
2274- tlsPort := cfg.Server.TLSPort
2275- redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2276- host := r.Host
2277- if h, _, err := net.SplitHostPort(host); err == nil {
2278- host = h
2279- } else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
2280- host = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]")
2281- }
2282- if tlsPort != 443 {
2283- host = net.JoinHostPort(host, strconv.Itoa(tlsPort))
2284- } else if strings.Contains(host, ":") {
2285- host = "[" + host + "]"
2286- }
2287- target := "https://" + host + r.RequestURI
2288- http.Redirect(w, r, target, http.StatusPermanentRedirect)
2289- })
2290-
2291- var redirectWithMiddleware http.Handler = redirectHandler
2292- if proxyCIDRMiddleware != nil {
2293- redirectWithMiddleware = proxyCIDRMiddleware(redirectHandler)
2294- }
2295- proxySrv, proxyReady := startServer(fmt.Sprintf(":%d", cfg.Server.Port), redirectWithMiddleware, "proxy-redirect", nil, errChan, log)
2296- tlsSrv, tlsReady := startTLSServer(
2297- fmt.Sprintf(":%d", cfg.Server.TLSPort),
2298- proxyHandler,
2299- "proxy-tls",
2300- cfg.Server.TLSCertFile,
2301- cfg.Server.TLSKeyFile,
2302- errChan,
2303- log,
2304- )
2305- return proxySrv, proxyReady, tlsSrv, tlsReady, nil
2306 }2132 }
2307 2133
2308 // startServer starts an HTTP server, returning the server instance and a channel2134 // startServer starts an HTTP server, returning the server instance and a channel
@@ -2635,10 +2461,7 @@ func isProcessAlive(pid int) bool {
2635 func loadConfig(v *viper.Viper, configPath string) error {2461 func loadConfig(v *viper.Viper, configPath string) error {
2636 v.SetDefault("server.port", 80)2462 v.SetDefault("server.port", 80)
2637 v.SetDefault("server.registry_port", 5000)2463 v.SetDefault("server.registry_port", 5000)
2638- v.SetDefault("server.tls_enabled", false)
2639 v.SetDefault("server.tls_port", 443)2464 v.SetDefault("server.tls_port", 443)
2640- v.SetDefault("server.tls_cert_file", "")
2641- v.SetDefault("server.tls_key_file", "")
2642 v.SetDefault("server.data_dir", DefaultDataDir())2465 v.SetDefault("server.data_dir", DefaultDataDir())
2643 v.SetDefault("server.runtime", "auto")2466 v.SetDefault("server.runtime", "auto")
2644 v.SetDefault("logging.level", "info")2467 v.SetDefault("logging.level", "info")
@@ -2690,7 +2513,6 @@ func loadConfig(v *viper.Viper, configPath string) error {
2690 v.SetDefault("server.registry_allowed_ips", []string{})2513 v.SetDefault("server.registry_allowed_ips", []string{})
2691 v.SetDefault("server.proxy_allowed_ips", []string{})2514 v.SetDefault("server.proxy_allowed_ips", []string{})
2692 v.SetDefault("server.registry_listen_address", "")2515 v.SetDefault("server.registry_listen_address", "")
2693- v.SetDefault("server.force_hsts", false)
2694 v.SetDefault("deploy.readiness_delay", "5s")2516 v.SetDefault("deploy.readiness_delay", "5s")
2695 v.SetDefault("deploy.readiness_mode", "auto")2517 v.SetDefault("deploy.readiness_mode", "auto")
2696 v.SetDefault("deploy.health_timeout", "90s")2518 v.SetDefault("deploy.health_timeout", "90s")

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