feat: add native TLS listener support
Allow Gordon to terminate HTTPS directly via server.tls_enabled, with optional auto-generated self-signed certificates when no cert/key paths are provided. Update docs to reflect the new TLS configuration options.
- commit
- df419f47802bcfba808697d35df32f23821d8638
- author
- brice · · 2026-02-07 04:42 UTC
- committer
- brice · · 2026-02-07 04:54 UTC
- parent
- 4a08931
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +237 -7
M docs/config/rate-limiting.md +2 -2
| @@ -28,7 +28,7 @@ burst = 100 | |||
| 28 | 28 | trusted_proxies = [] | |
| 29 | 29 | ``` | |
| 30 | 30 | ||
| 31 | - | > **Important:** Gordon does not handle TLS termination, so production deployments require a front proxy (Cloudflare, nginx, etc.) for HTTPS. You **must** configure `trusted_proxies` to ensure rate limiting sees real client IPs instead of your proxy's IP. | |
| 31 | + | > **Important:** If you run Gordon behind a front proxy (Cloudflare, nginx, etc.), configure `trusted_proxies` so rate limiting sees real client IPs instead of your proxy's IP. | |
| 32 | 32 | ||
| 33 | 33 | ## Configuration | |
| 34 | 34 | ||
| @@ -150,7 +150,7 @@ A burst of `100` with `per_ip_rps = 50` means a client can send 100 requests ins | |||
| 150 | 150 | ||
| 151 | 151 | **Critical for correct IP detection in production.** | |
| 152 | 152 | ||
| 153 | - | Gordon is a reverse proxy that routes requests to your containers, but it **does not handle TLS termination**. For HTTPS support, you must place Gordon behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer. | |
| 153 | + | Gordon is a reverse proxy that routes requests to your containers. In production you can either terminate TLS directly in Gordon (`server.tls_enabled = true`) or place it behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer. | |
| 154 | 154 | ||
| 155 | 155 | ``` | |
| 156 | 156 | Internet → [Cloudflare/nginx] → Gordon → Containers | |
| @@ -28,7 +28,7 @@ burst = 100 | |||||
| 28 | trusted_proxies = [] | 28 | trusted_proxies = [] | ||
| 29 | ``` | 29 | ``` | ||
| 30 | 30 | ||||
| 31 | - | > **Important:** Gordon does not handle TLS termination, so production deployments require a front proxy (Cloudflare, nginx, etc.) for HTTPS. You **must** configure `trusted_proxies` to ensure rate limiting sees real client IPs instead of your proxy's IP. | 31 | + | > **Important:** If you run Gordon behind a front proxy (Cloudflare, nginx, etc.), configure `trusted_proxies` so rate limiting sees real client IPs instead of your proxy's IP. |
| 32 | 32 | ||||
| 33 | ## Configuration | 33 | ## Configuration | ||
| 34 | 34 | ||||
| @@ -150,7 +150,7 @@ A burst of `100` with `per_ip_rps = 50` means a client can send 100 requests ins | |||||
| 150 | 150 | ||||
| 151 | **Critical for correct IP detection in production.** | 151 | **Critical for correct IP detection in production.** | ||
| 152 | 152 | ||||
| 153 | - | Gordon is a reverse proxy that routes requests to your containers, but it **does not handle TLS termination**. For HTTPS support, you must place Gordon behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer. | 153 | + | Gordon is a reverse proxy that routes requests to your containers. In production you can either terminate TLS directly in Gordon (`server.tls_enabled = true`) or place it behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer. |
| 154 | 154 | ||||
| 155 | ``` | 155 | ``` | ||
| 156 | Internet → [Cloudflare/nginx] → Gordon → Containers | 156 | Internet → [Cloudflare/nginx] → Gordon → Containers | ||
M docs/config/reference.md +8 -0
| @@ -11,6 +11,10 @@ Complete configuration reference with all options and their default values. | |||
| 11 | 11 | [server] | |
| 12 | 12 | port = 80 # HTTP proxy port | |
| 13 | 13 | registry_port = 5000 # Container registry port | |
| 14 | + | tls_enabled = false # Enable native HTTPS listener | |
| 15 | + | tls_port = 443 # HTTPS proxy port when enabled | |
| 16 | + | tls_cert_file = "" # PEM cert path (auto-generated if empty and TLS enabled) | |
| 17 | + | tls_key_file = "" # PEM key path (auto-generated if empty and TLS enabled) | |
| 14 | 18 | gordon_domain = "" # Required: Gordon domain (registry + API) | |
| 15 | 19 | data_dir = "~/.gordon" # Data directory (varies by install type) | |
| 16 | 20 | max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk | |
| @@ -124,6 +128,10 @@ preserve = true # Keep volumes when containers are | |||
| 124 | 128 | |---------|---------|-------------| | |
| 125 | 129 | | `server.port` | `80` | HTTP proxy port | | |
| 126 | 130 | | `server.registry_port` | `5000` | Container registry port | | |
| 131 | + | | `server.tls_enabled` | `false` | Enable native HTTPS listener | | |
| 132 | + | | `server.tls_port` | `443` | HTTPS listener port | | |
| 133 | + | | `server.tls_cert_file` | `""` | TLS cert path (auto-generated when empty) | | |
| 134 | + | | `server.tls_key_file` | `""` | TLS key path (auto-generated when empty) | | |
| 127 | 135 | | `server.gordon_domain` | `""` | **Required** - Gordon domain | | |
| 128 | 136 | | `server.data_dir` | `~/.gordon` | Data directory | | |
| 129 | 137 | | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk | | |
| @@ -11,6 +11,10 @@ Complete configuration reference with all options and their default values. | |||||
| 11 | [server] | 11 | [server] | ||
| 12 | port = 80 # HTTP proxy port | 12 | port = 80 # HTTP proxy port | ||
| 13 | registry_port = 5000 # Container registry port | 13 | registry_port = 5000 # Container registry port | ||
| 14 | + | tls_enabled = false # Enable native HTTPS listener | |||
| 15 | + | tls_port = 443 # HTTPS proxy port when enabled | |||
| 16 | + | tls_cert_file = "" # PEM cert path (auto-generated if empty and TLS enabled) | |||
| 17 | + | tls_key_file = "" # PEM key path (auto-generated if empty and TLS enabled) | |||
| 14 | gordon_domain = "" # Required: Gordon domain (registry + API) | 18 | gordon_domain = "" # Required: Gordon domain (registry + API) | ||
| 15 | data_dir = "~/.gordon" # Data directory (varies by install type) | 19 | data_dir = "~/.gordon" # Data directory (varies by install type) | ||
| 16 | max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk | 20 | max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk | ||
| @@ -124,6 +128,10 @@ preserve = true # Keep volumes when containers are | |||||
| 124 | |---------|---------|-------------| | 128 | |---------|---------|-------------| | ||
| 125 | | `server.port` | `80` | HTTP proxy port | | 129 | | `server.port` | `80` | HTTP proxy port | | ||
| 126 | | `server.registry_port` | `5000` | Container registry port | | 130 | | `server.registry_port` | `5000` | Container registry port | | ||
| 131 | + | | `server.tls_enabled` | `false` | Enable native HTTPS listener | | |||
| 132 | + | | `server.tls_port` | `443` | HTTPS listener port | | |||
| 133 | + | | `server.tls_cert_file` | `""` | TLS cert path (auto-generated when empty) | | |||
| 134 | + | | `server.tls_key_file` | `""` | TLS key path (auto-generated when empty) | | |||
| 127 | | `server.gordon_domain` | `""` | **Required** - Gordon domain | | 135 | | `server.gordon_domain` | `""` | **Required** - Gordon domain | | ||
| 128 | | `server.data_dir` | `~/.gordon` | Data directory | | 136 | | `server.data_dir` | `~/.gordon` | Data directory | | ||
| 129 | | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk | | 137 | | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk | | ||
M docs/config/server.md +26 -0
| @@ -8,6 +8,10 @@ Core server settings for Gordon. | |||
| 8 | 8 | [server] | |
| 9 | 9 | port = 8080 # HTTP proxy port | |
| 10 | 10 | registry_port = 5000 # Container registry port | |
| 11 | + | tls_enabled = false # Enable native TLS listener | |
| 12 | + | tls_port = 443 # HTTPS listener port | |
| 13 | + | tls_cert_file = "" # PEM certificate file (required when TLS enabled) | |
| 14 | + | tls_key_file = "" # PEM key file (required when TLS enabled) | |
| 11 | 15 | gordon_domain = "gordon.mydomain.com" # Gordon domain (required) | |
| 12 | 16 | # data_dir = "~/.gordon" # Data storage directory (default) | |
| 13 | 17 | ``` | |
| @@ -18,6 +22,10 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required) | |||
| 18 | 22 | |--------|------|---------|-------------| | |
| 19 | 23 | | `port` | int | `80` | HTTP proxy port for routing traffic to containers | | |
| 20 | 24 | | `registry_port` | int | `5000` | Docker registry port for image push/pull | | |
| 25 | + | | `tls_enabled` | bool | `false` | Enable native HTTPS listener for proxy traffic | | |
| 26 | + | | `tls_port` | int | `443` | HTTPS listener port when `tls_enabled` is true | | |
| 27 | + | | `tls_cert_file` | string | `""` | Path to PEM certificate file | | |
| 28 | + | | `tls_key_file` | string | `""` | Path to PEM private key file | | |
| 21 | 29 | | `gordon_domain` | string | **required** | Domain for Gordon (registry + admin API) | | |
| 22 | 30 | | `registry_domain` | string | - | Deprecated alias for `gordon_domain` | | |
| 23 | 31 | | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files | | |
| @@ -42,6 +50,24 @@ For rootless containers, you'll typically use a high port and configure firewall | |||
| 42 | 50 | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080 | |
| 43 | 51 | ``` | |
| 44 | 52 | ||
| 53 | + | ### Native TLS Listener | |
| 54 | + | ||
| 55 | + | Enable native HTTPS directly in Gordon: | |
| 56 | + | ||
| 57 | + | ```toml | |
| 58 | + | [server] | |
| 59 | + | tls_enabled = true | |
| 60 | + | tls_port = 443 | |
| 61 | + | tls_cert_file = "/etc/gordon/certs/fullchain.pem" | |
| 62 | + | tls_key_file = "/etc/gordon/certs/privkey.pem" | |
| 63 | + | ``` | |
| 64 | + | ||
| 65 | + | With TLS enabled: | |
| 66 | + | - Gordon terminates HTTPS on `tls_port` | |
| 67 | + | - Host-based routing still works (apps, `/v2/*`, `/auth/*`, `/admin/*`) | |
| 68 | + | - `port` remains available for plain HTTP unless you restrict it externally | |
| 69 | + | - If `tls_cert_file`/`tls_key_file` are empty, Gordon auto-generates a self-signed pair in `{data_dir}/tls/` | |
| 70 | + | ||
| 45 | 71 | ### Registry Port | |
| 46 | 72 | ||
| 47 | 73 | The `registry_port` setting controls where Gordon accepts image pushes: | |
| @@ -8,6 +8,10 @@ Core server settings for Gordon. | |||||
| 8 | [server] | 8 | [server] | ||
| 9 | port = 8080 # HTTP proxy port | 9 | port = 8080 # HTTP proxy port | ||
| 10 | registry_port = 5000 # Container registry port | 10 | registry_port = 5000 # Container registry port | ||
| 11 | + | tls_enabled = false # Enable native TLS listener | |||
| 12 | + | tls_port = 443 # HTTPS listener port | |||
| 13 | + | tls_cert_file = "" # PEM certificate file (required when TLS enabled) | |||
| 14 | + | tls_key_file = "" # PEM key file (required when TLS enabled) | |||
| 11 | gordon_domain = "gordon.mydomain.com" # Gordon domain (required) | 15 | gordon_domain = "gordon.mydomain.com" # Gordon domain (required) | ||
| 12 | # data_dir = "~/.gordon" # Data storage directory (default) | 16 | # data_dir = "~/.gordon" # Data storage directory (default) | ||
| 13 | ``` | 17 | ``` | ||
| @@ -18,6 +22,10 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required) | |||||
| 18 | |--------|------|---------|-------------| | 22 | |--------|------|---------|-------------| | ||
| 19 | | `port` | int | `80` | HTTP proxy port for routing traffic to containers | | 23 | | `port` | int | `80` | HTTP proxy port for routing traffic to containers | | ||
| 20 | | `registry_port` | int | `5000` | Docker registry port for image push/pull | | 24 | | `registry_port` | int | `5000` | Docker registry port for image push/pull | | ||
| 25 | + | | `tls_enabled` | bool | `false` | Enable native HTTPS listener for proxy traffic | | |||
| 26 | + | | `tls_port` | int | `443` | HTTPS listener port when `tls_enabled` is true | | |||
| 27 | + | | `tls_cert_file` | string | `""` | Path to PEM certificate file | | |||
| 28 | + | | `tls_key_file` | string | `""` | Path to PEM private key file | | |||
| 21 | | `gordon_domain` | string | **required** | Domain for Gordon (registry + admin API) | | 29 | | `gordon_domain` | string | **required** | Domain for Gordon (registry + admin API) | | ||
| 22 | | `registry_domain` | string | - | Deprecated alias for `gordon_domain` | | 30 | | `registry_domain` | string | - | Deprecated alias for `gordon_domain` | | ||
| 23 | | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files | | 31 | | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files | | ||
| @@ -42,6 +50,24 @@ For rootless containers, you'll typically use a high port and configure firewall | |||||
| 42 | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080 | 50 | sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080 | ||
| 43 | ``` | 51 | ``` | ||
| 44 | 52 | ||||
| 53 | + | ### Native TLS Listener | |||
| 54 | + | ||||
| 55 | + | Enable native HTTPS directly in Gordon: | |||
| 56 | + | ||||
| 57 | + | ```toml | |||
| 58 | + | [server] | |||
| 59 | + | tls_enabled = true | |||
| 60 | + | tls_port = 443 | |||
| 61 | + | tls_cert_file = "/etc/gordon/certs/fullchain.pem" | |||
| 62 | + | tls_key_file = "/etc/gordon/certs/privkey.pem" | |||
| 63 | + | ``` | |||
| 64 | + | ||||
| 65 | + | With TLS enabled: | |||
| 66 | + | - Gordon terminates HTTPS on `tls_port` | |||
| 67 | + | - Host-based routing still works (apps, `/v2/*`, `/auth/*`, `/admin/*`) | |||
| 68 | + | - `port` remains available for plain HTTP unless you restrict it externally | |||
| 69 | + | - If `tls_cert_file`/`tls_key_file` are empty, Gordon auto-generates a self-signed pair in `{data_dir}/tls/` | |||
| 70 | + | ||||
| 45 | ### Registry Port | 71 | ### Registry Port | ||
| 46 | 72 | ||||
| 47 | The `registry_port` setting controls where Gordon accepts image pushes: | 73 | The `registry_port` setting controls where Gordon accepts image pushes: | ||
M docs/index.md +1 -1
| @@ -77,7 +77,7 @@ Gordon runs on your VPS and provides: | |||
| 77 | 77 | - Domain pointing to your server | |
| 78 | 78 | - Cloudflare account for HTTPS (free tier works) | |
| 79 | 79 | ||
| 80 | - | > **Note:** Gordon does not handle TLS certificates directly. HTTPS is terminated by Cloudflare (or similar proxy) which forwards HTTP traffic to Gordon internally. | |
| 80 | + | > **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly when `server.tls_enabled = true`. | |
| 81 | 81 | ||
| 82 | 82 | ## Related | |
| 83 | 83 | ||
| @@ -77,7 +77,7 @@ Gordon runs on your VPS and provides: | |||||
| 77 | - Domain pointing to your server | 77 | - Domain pointing to your server | ||
| 78 | - Cloudflare account for HTTPS (free tier works) | 78 | - Cloudflare account for HTTPS (free tier works) | ||
| 79 | 79 | ||||
| 80 | - | > **Note:** Gordon does not handle TLS certificates directly. HTTPS is terminated by Cloudflare (or similar proxy) which forwards HTTP traffic to Gordon internally. | 80 | + | > **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly when `server.tls_enabled = true`. |
| 81 | 81 | ||||
| 82 | ## Related | 82 | ## Related | ||
| 83 | 83 | ||||
M internal/app/run.go +200 -4
| @@ -3,11 +3,17 @@ package app | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "context" | |
| 6 | + | "crypto/ecdsa" | |
| 7 | + | "crypto/elliptic" | |
| 6 | 8 | "crypto/rand" | |
| 9 | + | "crypto/x509" | |
| 10 | + | "crypto/x509/pkix" | |
| 7 | 11 | "encoding/hex" | |
| 8 | 12 | "encoding/json" | |
| 13 | + | "encoding/pem" | |
| 9 | 14 | "errors" | |
| 10 | 15 | "fmt" | |
| 16 | + | "math/big" | |
| 11 | 17 | "net" | |
| 12 | 18 | "net/http" | |
| 13 | 19 | "os" | |
| @@ -68,6 +74,10 @@ type Config struct { | |||
| 68 | 74 | RegistryPort int `mapstructure:"registry_port"` | |
| 69 | 75 | GordonDomain string `mapstructure:"gordon_domain"` | |
| 70 | 76 | RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain | |
| 77 | + | TLSEnabled bool `mapstructure:"tls_enabled"` | |
| 78 | + | TLSPort int `mapstructure:"tls_port"` | |
| 79 | + | TLSCertFile string `mapstructure:"tls_cert_file"` | |
| 80 | + | TLSKeyFile string `mapstructure:"tls_key_file"` | |
| 71 | 81 | DataDir string `mapstructure:"data_dir"` | |
| 72 | 82 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | |
| 73 | 83 | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | |
| @@ -160,6 +170,10 @@ func Run(ctx context.Context, configPath string) error { | |||
| 160 | 170 | ctx = zerowrap.WithCtx(ctx, log) | |
| 161 | 171 | log.Info().Msg("Gordon starting") | |
| 162 | 172 | ||
| 173 | + | if err := ensureTLSConfig(&cfg, log); err != nil { | |
| 174 | + | return err | |
| 175 | + | } | |
| 176 | + | ||
| 163 | 177 | // Create PID file | |
| 164 | 178 | pidFile := createPidFile(log) | |
| 165 | 179 | if pidFile != "" { | |
| @@ -193,6 +207,123 @@ func Run(ctx context.Context, configPath string) error { | |||
| 193 | 207 | return runServers(ctx, cfg, registryHandler, proxyHandler, svc.containerSvc, svc.eventBus, svc, log) | |
| 194 | 208 | } | |
| 195 | 209 | ||
| 210 | + | func ensureTLSConfig(cfg *Config, log zerowrap.Logger) error { | |
| 211 | + | if !cfg.Server.TLSEnabled { | |
| 212 | + | return nil | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | if cfg.Server.TLSCertFile != "" && cfg.Server.TLSKeyFile != "" { | |
| 216 | + | return nil | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | dataDir := resolveDataDir(cfg.Server.DataDir) | |
| 220 | + | tlsDir := filepath.Join(dataDir, "tls") | |
| 221 | + | certPath := filepath.Join(tlsDir, "cert.pem") | |
| 222 | + | keyPath := filepath.Join(tlsDir, "key.pem") | |
| 223 | + | ||
| 224 | + | domainName := cfg.Server.GordonDomain | |
| 225 | + | if domainName == "" { | |
| 226 | + | domainName = cfg.Server.RegistryDomain | |
| 227 | + | } | |
| 228 | + | if domainName == "" { | |
| 229 | + | domainName = "localhost" | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | if err := generateSelfSignedTLSCert(certPath, keyPath, domainName); err != nil { | |
| 233 | + | return fmt.Errorf("failed to generate self-signed TLS certificate: %w", err) | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | cfg.Server.TLSCertFile = certPath | |
| 237 | + | cfg.Server.TLSKeyFile = keyPath | |
| 238 | + | ||
| 239 | + | log.Warn(). | |
| 240 | + | Str("cert_file", certPath). | |
| 241 | + | Str("key_file", keyPath). | |
| 242 | + | Str("domain", domainName). | |
| 243 | + | Msg("server.tls_enabled=true with empty tls_cert_file/tls_key_file; using auto-generated self-signed certificate") | |
| 244 | + | ||
| 245 | + | return nil | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | func generateSelfSignedTLSCert(certPath, keyPath, domainName string) error { | |
| 249 | + | // Reuse existing pair if already generated. | |
| 250 | + | if _, err := os.Stat(certPath); err == nil { | |
| 251 | + | if _, keyErr := os.Stat(keyPath); keyErr == nil { | |
| 252 | + | return nil | |
| 253 | + | } | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | if err := os.MkdirAll(filepath.Dir(certPath), 0700); err != nil { | |
| 257 | + | return err | |
| 258 | + | } | |
| 259 | + | if err := os.MkdirAll(filepath.Dir(keyPath), 0700); err != nil { | |
| 260 | + | return err | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |
| 264 | + | if err != nil { | |
| 265 | + | return err | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | now := time.Now() | |
| 269 | + | serialLimit := new(big.Int).Lsh(big.NewInt(1), 128) | |
| 270 | + | serialNumber, err := rand.Int(rand.Reader, serialLimit) | |
| 271 | + | if err != nil { | |
| 272 | + | return err | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | template := x509.Certificate{ | |
| 276 | + | SerialNumber: serialNumber, | |
| 277 | + | Subject: pkix.Name{ | |
| 278 | + | CommonName: domainName, | |
| 279 | + | Organization: []string{"Gordon Auto-Generated TLS"}, | |
| 280 | + | }, | |
| 281 | + | NotBefore: now.Add(-5 * time.Minute), | |
| 282 | + | NotAfter: now.AddDate(1, 0, 0), | |
| 283 | + | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, | |
| 284 | + | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | |
| 285 | + | BasicConstraintsValid: true, | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | if ip := net.ParseIP(domainName); ip != nil { | |
| 289 | + | template.IPAddresses = []net.IP{ip} | |
| 290 | + | } else { | |
| 291 | + | template.DNSNames = []string{domainName} | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | |
| 295 | + | if err != nil { | |
| 296 | + | return err | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) | |
| 300 | + | if err != nil { | |
| 301 | + | return err | |
| 302 | + | } | |
| 303 | + | defer certOut.Close() | |
| 304 | + | ||
| 305 | + | if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { | |
| 306 | + | return err | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | keyBytes, err := x509.MarshalECPrivateKey(priv) | |
| 310 | + | if err != nil { | |
| 311 | + | return err | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) | |
| 315 | + | if err != nil { | |
| 316 | + | return err | |
| 317 | + | } | |
| 318 | + | defer keyOut.Close() | |
| 319 | + | ||
| 320 | + | if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}); err != nil { | |
| 321 | + | return err | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | return nil | |
| 325 | + | } | |
| 326 | + | ||
| 196 | 327 | // initConfig loads configuration from file. | |
| 197 | 328 | func initConfig(configPath string) (*viper.Viper, Config, error) { | |
| 198 | 329 | v := viper.New() | |
| @@ -1100,10 +1231,25 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||
| 1100 | 1231 | signal.Notify(deployChan, syscall.SIGUSR2) | |
| 1101 | 1232 | defer signal.Stop(deployChan) | |
| 1102 | 1233 | ||
| 1103 | - | errChan := make(chan error, 2) | |
| 1234 | + | errChan := make(chan error, 3) | |
| 1104 | 1235 | ||
| 1105 | 1236 | registryReady := startServer(fmt.Sprintf(":%d", cfg.Server.RegistryPort), registryHandler, "registry", errChan, log) | |
| 1106 | 1237 | proxyReady := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", errChan, log) | |
| 1238 | + | var tlsReady <-chan struct{} | |
| 1239 | + | if cfg.Server.TLSEnabled { | |
| 1240 | + | if cfg.Server.TLSCertFile == "" || cfg.Server.TLSKeyFile == "" { | |
| 1241 | + | return fmt.Errorf("server.tls_enabled=true requires both server.tls_cert_file and server.tls_key_file") | |
| 1242 | + | } | |
| 1243 | + | tlsReady = startTLSServer( | |
| 1244 | + | fmt.Sprintf(":%d", cfg.Server.TLSPort), | |
| 1245 | + | proxyHandler, | |
| 1246 | + | "proxy-tls", | |
| 1247 | + | cfg.Server.TLSCertFile, | |
| 1248 | + | cfg.Server.TLSKeyFile, | |
| 1249 | + | errChan, | |
| 1250 | + | log, | |
| 1251 | + | ) | |
| 1252 | + | } | |
| 1107 | 1253 | ||
| 1108 | 1254 | // Wait for both servers to bind their ports before auto-starting containers. | |
| 1109 | 1255 | // This prevents the race where auto-start pulls from the registry before it's listening. | |
| @@ -1117,11 +1263,21 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||
| 1117 | 1263 | case err := <-errChan: | |
| 1118 | 1264 | return err | |
| 1119 | 1265 | } | |
| 1266 | + | if cfg.Server.TLSEnabled { | |
| 1267 | + | select { | |
| 1268 | + | case <-tlsReady: | |
| 1269 | + | case err := <-errChan: | |
| 1270 | + | return err | |
| 1271 | + | } | |
| 1272 | + | } | |
| 1120 | 1273 | ||
| 1121 | - | log.Info(). | |
| 1274 | + | logEvent := log.Info(). | |
| 1122 | 1275 | Int("proxy_port", cfg.Server.Port). | |
| 1123 | - | Int("registry_port", cfg.Server.RegistryPort). | |
| 1124 | - | Msg("Gordon is running") | |
| 1276 | + | Int("registry_port", cfg.Server.RegistryPort) | |
| 1277 | + | if cfg.Server.TLSEnabled { | |
| 1278 | + | logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort) | |
| 1279 | + | } | |
| 1280 | + | logEvent.Msg("Gordon is running") | |
| 1125 | 1281 | ||
| 1126 | 1282 | // Auto-start after servers are listening (registry port is now bound). | |
| 1127 | 1283 | syncAndAutoStart(ctx, svc, log) | |
| @@ -1201,6 +1357,42 @@ func startServer(addr string, handler http.Handler, name string, errChan chan<- | |||
| 1201 | 1357 | return ready | |
| 1202 | 1358 | } | |
| 1203 | 1359 | ||
| 1360 | + | // startTLSServer starts an HTTPS server with the provided certificate and key. | |
| 1361 | + | func startTLSServer(addr string, handler http.Handler, name, certFile, keyFile string, errChan chan<- error, log zerowrap.Logger) <-chan struct{} { | |
| 1362 | + | ready := make(chan struct{}) | |
| 1363 | + | ||
| 1364 | + | go func() { | |
| 1365 | + | log.Info(). | |
| 1366 | + | Str("address", addr). | |
| 1367 | + | Str("cert_file", certFile). | |
| 1368 | + | Str("key_file", keyFile). | |
| 1369 | + | Msgf("%s server starting", name) | |
| 1370 | + | ||
| 1371 | + | server := &http.Server{ | |
| 1372 | + | Addr: addr, | |
| 1373 | + | Handler: handler, | |
| 1374 | + | ReadHeaderTimeout: 10 * time.Second, | |
| 1375 | + | ReadTimeout: 5 * time.Minute, | |
| 1376 | + | WriteTimeout: 5 * time.Minute, | |
| 1377 | + | IdleTimeout: 120 * time.Second, | |
| 1378 | + | MaxHeaderBytes: 1 << 20, | |
| 1379 | + | } | |
| 1380 | + | ||
| 1381 | + | ln, err := net.Listen("tcp", addr) | |
| 1382 | + | if err != nil { | |
| 1383 | + | errChan <- fmt.Errorf("%s server error: %w", name, err) | |
| 1384 | + | return | |
| 1385 | + | } | |
| 1386 | + | close(ready) | |
| 1387 | + | ||
| 1388 | + | if err := server.ServeTLS(ln, certFile, keyFile); err != nil && err != http.ErrServerClosed { | |
| 1389 | + | errChan <- fmt.Errorf("%s server error: %w", name, err) | |
| 1390 | + | } | |
| 1391 | + | }() | |
| 1392 | + | ||
| 1393 | + | return ready | |
| 1394 | + | } | |
| 1395 | + | ||
| 1204 | 1396 | // SendReloadSignal sends SIGUSR1 to the running Gordon process. | |
| 1205 | 1397 | func SendReloadSignal() error { | |
| 1206 | 1398 | process, _, err := findRunningProcess() | |
| @@ -1454,6 +1646,10 @@ func isProcessAlive(pid int) bool { | |||
| 1454 | 1646 | func loadConfig(v *viper.Viper, configPath string) error { | |
| 1455 | 1647 | v.SetDefault("server.port", 80) | |
| 1456 | 1648 | v.SetDefault("server.registry_port", 5000) | |
| 1649 | + | v.SetDefault("server.tls_enabled", false) | |
| 1650 | + | v.SetDefault("server.tls_port", 443) | |
| 1651 | + | v.SetDefault("server.tls_cert_file", "") | |
| 1652 | + | v.SetDefault("server.tls_key_file", "") | |
| 1457 | 1653 | v.SetDefault("server.data_dir", DefaultDataDir()) | |
| 1458 | 1654 | v.SetDefault("logging.level", "info") | |
| 1459 | 1655 | v.SetDefault("logging.format", "console") | |
| @@ -3,11 +3,17 @@ package app | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "context" | 5 | "context" | ||
| 6 | + | "crypto/ecdsa" | |||
| 7 | + | "crypto/elliptic" | |||
| 6 | "crypto/rand" | 8 | "crypto/rand" | ||
| 9 | + | "crypto/x509" | |||
| 10 | + | "crypto/x509/pkix" | |||
| 7 | "encoding/hex" | 11 | "encoding/hex" | ||
| 8 | "encoding/json" | 12 | "encoding/json" | ||
| 13 | + | "encoding/pem" | |||
| 9 | "errors" | 14 | "errors" | ||
| 10 | "fmt" | 15 | "fmt" | ||
| 16 | + | "math/big" | |||
| 11 | "net" | 17 | "net" | ||
| 12 | "net/http" | 18 | "net/http" | ||
| 13 | "os" | 19 | "os" | ||
| @@ -68,6 +74,10 @@ type Config struct { | |||||
| 68 | RegistryPort int `mapstructure:"registry_port"` | 74 | RegistryPort int `mapstructure:"registry_port"` | ||
| 69 | GordonDomain string `mapstructure:"gordon_domain"` | 75 | GordonDomain string `mapstructure:"gordon_domain"` | ||
| 70 | RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain | 76 | RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain | ||
| 77 | + | TLSEnabled bool `mapstructure:"tls_enabled"` | |||
| 78 | + | TLSPort int `mapstructure:"tls_port"` | |||
| 79 | + | TLSCertFile string `mapstructure:"tls_cert_file"` | |||
| 80 | + | TLSKeyFile string `mapstructure:"tls_key_file"` | |||
| 71 | DataDir string `mapstructure:"data_dir"` | 81 | DataDir string `mapstructure:"data_dir"` | ||
| 72 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | 82 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | ||
| 73 | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | 83 | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | ||
| @@ -160,6 +170,10 @@ func Run(ctx context.Context, configPath string) error { | |||||
| 160 | ctx = zerowrap.WithCtx(ctx, log) | 170 | ctx = zerowrap.WithCtx(ctx, log) | ||
| 161 | log.Info().Msg("Gordon starting") | 171 | log.Info().Msg("Gordon starting") | ||
| 162 | 172 | ||||
| 173 | + | if err := ensureTLSConfig(&cfg, log); err != nil { | |||
| 174 | + | return err | |||
| 175 | + | } | |||
| 176 | + | ||||
| 163 | // Create PID file | 177 | // Create PID file | ||
| 164 | pidFile := createPidFile(log) | 178 | pidFile := createPidFile(log) | ||
| 165 | if pidFile != "" { | 179 | if pidFile != "" { | ||
| @@ -193,6 +207,123 @@ func Run(ctx context.Context, configPath string) error { | |||||
| 193 | return runServers(ctx, cfg, registryHandler, proxyHandler, svc.containerSvc, svc.eventBus, svc, log) | 207 | return runServers(ctx, cfg, registryHandler, proxyHandler, svc.containerSvc, svc.eventBus, svc, log) | ||
| 194 | } | 208 | } | ||
| 195 | 209 | ||||
| 210 | + | func ensureTLSConfig(cfg *Config, log zerowrap.Logger) error { | |||
| 211 | + | if !cfg.Server.TLSEnabled { | |||
| 212 | + | return nil | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | if cfg.Server.TLSCertFile != "" && cfg.Server.TLSKeyFile != "" { | |||
| 216 | + | return nil | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | dataDir := resolveDataDir(cfg.Server.DataDir) | |||
| 220 | + | tlsDir := filepath.Join(dataDir, "tls") | |||
| 221 | + | certPath := filepath.Join(tlsDir, "cert.pem") | |||
| 222 | + | keyPath := filepath.Join(tlsDir, "key.pem") | |||
| 223 | + | ||||
| 224 | + | domainName := cfg.Server.GordonDomain | |||
| 225 | + | if domainName == "" { | |||
| 226 | + | domainName = cfg.Server.RegistryDomain | |||
| 227 | + | } | |||
| 228 | + | if domainName == "" { | |||
| 229 | + | domainName = "localhost" | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | if err := generateSelfSignedTLSCert(certPath, keyPath, domainName); err != nil { | |||
| 233 | + | return fmt.Errorf("failed to generate self-signed TLS certificate: %w", err) | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | cfg.Server.TLSCertFile = certPath | |||
| 237 | + | cfg.Server.TLSKeyFile = keyPath | |||
| 238 | + | ||||
| 239 | + | log.Warn(). | |||
| 240 | + | Str("cert_file", certPath). | |||
| 241 | + | Str("key_file", keyPath). | |||
| 242 | + | Str("domain", domainName). | |||
| 243 | + | Msg("server.tls_enabled=true with empty tls_cert_file/tls_key_file; using auto-generated self-signed certificate") | |||
| 244 | + | ||||
| 245 | + | return nil | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | func generateSelfSignedTLSCert(certPath, keyPath, domainName string) error { | |||
| 249 | + | // Reuse existing pair if already generated. | |||
| 250 | + | if _, err := os.Stat(certPath); err == nil { | |||
| 251 | + | if _, keyErr := os.Stat(keyPath); keyErr == nil { | |||
| 252 | + | return nil | |||
| 253 | + | } | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | if err := os.MkdirAll(filepath.Dir(certPath), 0700); err != nil { | |||
| 257 | + | return err | |||
| 258 | + | } | |||
| 259 | + | if err := os.MkdirAll(filepath.Dir(keyPath), 0700); err != nil { | |||
| 260 | + | return err | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |||
| 264 | + | if err != nil { | |||
| 265 | + | return err | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | now := time.Now() | |||
| 269 | + | serialLimit := new(big.Int).Lsh(big.NewInt(1), 128) | |||
| 270 | + | serialNumber, err := rand.Int(rand.Reader, serialLimit) | |||
| 271 | + | if err != nil { | |||
| 272 | + | return err | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | template := x509.Certificate{ | |||
| 276 | + | SerialNumber: serialNumber, | |||
| 277 | + | Subject: pkix.Name{ | |||
| 278 | + | CommonName: domainName, | |||
| 279 | + | Organization: []string{"Gordon Auto-Generated TLS"}, | |||
| 280 | + | }, | |||
| 281 | + | NotBefore: now.Add(-5 * time.Minute), | |||
| 282 | + | NotAfter: now.AddDate(1, 0, 0), | |||
| 283 | + | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, | |||
| 284 | + | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | |||
| 285 | + | BasicConstraintsValid: true, | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | if ip := net.ParseIP(domainName); ip != nil { | |||
| 289 | + | template.IPAddresses = []net.IP{ip} | |||
| 290 | + | } else { | |||
| 291 | + | template.DNSNames = []string{domainName} | |||
| 292 | + | } | |||
| 293 | + | ||||
| 294 | + | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | |||
| 295 | + | if err != nil { | |||
| 296 | + | return err | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) | |||
| 300 | + | if err != nil { | |||
| 301 | + | return err | |||
| 302 | + | } | |||
| 303 | + | defer certOut.Close() | |||
| 304 | + | ||||
| 305 | + | if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { | |||
| 306 | + | return err | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | keyBytes, err := x509.MarshalECPrivateKey(priv) | |||
| 310 | + | if err != nil { | |||
| 311 | + | return err | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) | |||
| 315 | + | if err != nil { | |||
| 316 | + | return err | |||
| 317 | + | } | |||
| 318 | + | defer keyOut.Close() | |||
| 319 | + | ||||
| 320 | + | if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}); err != nil { | |||
| 321 | + | return err | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | return nil | |||
| 325 | + | } | |||
| 326 | + | ||||
| 196 | // initConfig loads configuration from file. | 327 | // initConfig loads configuration from file. | ||
| 197 | func initConfig(configPath string) (*viper.Viper, Config, error) { | 328 | func initConfig(configPath string) (*viper.Viper, Config, error) { | ||
| 198 | v := viper.New() | 329 | v := viper.New() | ||
| @@ -1100,10 +1231,25 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||||
| 1100 | signal.Notify(deployChan, syscall.SIGUSR2) | 1231 | signal.Notify(deployChan, syscall.SIGUSR2) | ||
| 1101 | defer signal.Stop(deployChan) | 1232 | defer signal.Stop(deployChan) | ||
| 1102 | 1233 | ||||
| 1103 | - | errChan := make(chan error, 2) | 1234 | + | errChan := make(chan error, 3) |
| 1104 | 1235 | ||||
| 1105 | registryReady := startServer(fmt.Sprintf(":%d", cfg.Server.RegistryPort), registryHandler, "registry", errChan, log) | 1236 | registryReady := startServer(fmt.Sprintf(":%d", cfg.Server.RegistryPort), registryHandler, "registry", errChan, log) | ||
| 1106 | proxyReady := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", errChan, log) | 1237 | proxyReady := startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", errChan, log) | ||
| 1238 | + | var tlsReady <-chan struct{} | |||
| 1239 | + | if cfg.Server.TLSEnabled { | |||
| 1240 | + | if cfg.Server.TLSCertFile == "" || cfg.Server.TLSKeyFile == "" { | |||
| 1241 | + | return fmt.Errorf("server.tls_enabled=true requires both server.tls_cert_file and server.tls_key_file") | |||
| 1242 | + | } | |||
| 1243 | + | tlsReady = startTLSServer( | |||
| 1244 | + | fmt.Sprintf(":%d", cfg.Server.TLSPort), | |||
| 1245 | + | proxyHandler, | |||
| 1246 | + | "proxy-tls", | |||
| 1247 | + | cfg.Server.TLSCertFile, | |||
| 1248 | + | cfg.Server.TLSKeyFile, | |||
| 1249 | + | errChan, | |||
| 1250 | + | log, | |||
| 1251 | + | ) | |||
| 1252 | + | } | |||
| 1107 | 1253 | ||||
| 1108 | // Wait for both servers to bind their ports before auto-starting containers. | 1254 | // Wait for both servers to bind their ports before auto-starting containers. | ||
| 1109 | // This prevents the race where auto-start pulls from the registry before it's listening. | 1255 | // This prevents the race where auto-start pulls from the registry before it's listening. | ||
| @@ -1117,11 +1263,21 @@ func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler h | |||||
| 1117 | case err := <-errChan: | 1263 | case err := <-errChan: | ||
| 1118 | return err | 1264 | return err | ||
| 1119 | } | 1265 | } | ||
| 1266 | + | if cfg.Server.TLSEnabled { | |||
| 1267 | + | select { | |||
| 1268 | + | case <-tlsReady: | |||
| 1269 | + | case err := <-errChan: | |||
| 1270 | + | return err | |||
| 1271 | + | } | |||
| 1272 | + | } | |||
| 1120 | 1273 | ||||
| 1121 | - | log.Info(). | 1274 | + | logEvent := log.Info(). |
| 1122 | Int("proxy_port", cfg.Server.Port). | 1275 | Int("proxy_port", cfg.Server.Port). | ||
| 1123 | - | Int("registry_port", cfg.Server.RegistryPort). | 1276 | + | Int("registry_port", cfg.Server.RegistryPort) |
| 1124 | - | Msg("Gordon is running") | 1277 | + | if cfg.Server.TLSEnabled { |
| 1278 | + | logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort) | |||
| 1279 | + | } | |||
| 1280 | + | logEvent.Msg("Gordon is running") | |||
| 1125 | 1281 | ||||
| 1126 | // Auto-start after servers are listening (registry port is now bound). | 1282 | // Auto-start after servers are listening (registry port is now bound). | ||
| 1127 | syncAndAutoStart(ctx, svc, log) | 1283 | syncAndAutoStart(ctx, svc, log) | ||
| @@ -1201,6 +1357,42 @@ func startServer(addr string, handler http.Handler, name string, errChan chan<- | |||||
| 1201 | return ready | 1357 | return ready | ||
| 1202 | } | 1358 | } | ||
| 1203 | 1359 | ||||
| 1360 | + | // startTLSServer starts an HTTPS server with the provided certificate and key. | |||
| 1361 | + | func startTLSServer(addr string, handler http.Handler, name, certFile, keyFile string, errChan chan<- error, log zerowrap.Logger) <-chan struct{} { | |||
| 1362 | + | ready := make(chan struct{}) | |||
| 1363 | + | ||||
| 1364 | + | go func() { | |||
| 1365 | + | log.Info(). | |||
| 1366 | + | Str("address", addr). | |||
| 1367 | + | Str("cert_file", certFile). | |||
| 1368 | + | Str("key_file", keyFile). | |||
| 1369 | + | Msgf("%s server starting", name) | |||
| 1370 | + | ||||
| 1371 | + | server := &http.Server{ | |||
| 1372 | + | Addr: addr, | |||
| 1373 | + | Handler: handler, | |||
| 1374 | + | ReadHeaderTimeout: 10 * time.Second, | |||
| 1375 | + | ReadTimeout: 5 * time.Minute, | |||
| 1376 | + | WriteTimeout: 5 * time.Minute, | |||
| 1377 | + | IdleTimeout: 120 * time.Second, | |||
| 1378 | + | MaxHeaderBytes: 1 << 20, | |||
| 1379 | + | } | |||
| 1380 | + | ||||
| 1381 | + | ln, err := net.Listen("tcp", addr) | |||
| 1382 | + | if err != nil { | |||
| 1383 | + | errChan <- fmt.Errorf("%s server error: %w", name, err) | |||
| 1384 | + | return | |||
| 1385 | + | } | |||
| 1386 | + | close(ready) | |||
| 1387 | + | ||||
| 1388 | + | if err := server.ServeTLS(ln, certFile, keyFile); err != nil && err != http.ErrServerClosed { | |||
| 1389 | + | errChan <- fmt.Errorf("%s server error: %w", name, err) | |||
| 1390 | + | } | |||
| 1391 | + | }() | |||
| 1392 | + | ||||
| 1393 | + | return ready | |||
| 1394 | + | } | |||
| 1395 | + | ||||
| 1204 | // SendReloadSignal sends SIGUSR1 to the running Gordon process. | 1396 | // SendReloadSignal sends SIGUSR1 to the running Gordon process. | ||
| 1205 | func SendReloadSignal() error { | 1397 | func SendReloadSignal() error { | ||
| 1206 | process, _, err := findRunningProcess() | 1398 | process, _, err := findRunningProcess() | ||
| @@ -1454,6 +1646,10 @@ func isProcessAlive(pid int) bool { | |||||
| 1454 | func loadConfig(v *viper.Viper, configPath string) error { | 1646 | func loadConfig(v *viper.Viper, configPath string) error { | ||
| 1455 | v.SetDefault("server.port", 80) | 1647 | v.SetDefault("server.port", 80) | ||
| 1456 | v.SetDefault("server.registry_port", 5000) | 1648 | v.SetDefault("server.registry_port", 5000) | ||
| 1649 | + | v.SetDefault("server.tls_enabled", false) | |||
| 1650 | + | v.SetDefault("server.tls_port", 443) | |||
| 1651 | + | v.SetDefault("server.tls_cert_file", "") | |||
| 1652 | + | v.SetDefault("server.tls_key_file", "") | |||
| 1457 | v.SetDefault("server.data_dir", DefaultDataDir()) | 1653 | v.SetDefault("server.data_dir", DefaultDataDir()) | ||
| 1458 | v.SetDefault("logging.level", "info") | 1654 | v.SetDefault("logging.level", "info") | ||
| 1459 | v.SetDefault("logging.format", "console") | 1655 | v.SetDefault("logging.format", "console") | ||