diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index c8cb5c81..2ea0e162 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -68,7 +68,10 @@ func initEngine() (*gin.Engine, error) { r := gin.New() initLogger(r) - configureEngine(r) + err := configureEngine(r) + if err != nil { + return nil, err + } registerGlobalMiddleware(r) return r, nil @@ -86,9 +89,10 @@ func setGinMode() { } } -func configureEngine(r *gin.Engine) { - if !common.EnvConfig.TrustProxy { - _ = r.SetTrustedProxies(nil) +func configureEngine(r *gin.Engine) error { + err := r.SetTrustedProxies(common.EnvConfig.TrustProxy) + if err != nil { + return fmt.Errorf("failed to configure trusted proxies: %w", err) } if common.EnvConfig.TrustedPlatform != "" { @@ -99,6 +103,8 @@ func configureEngine(r *gin.Engine) { common.Name, otelgin.WithFilter(shouldTraceRequest)), ) + + return nil } // shouldTraceRequest reports whether an incoming request should be traced. diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 1f040596..601f92da 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "reflect" + "strconv" "strings" "github.com/caarlos0/env/v11" @@ -17,6 +18,7 @@ import ( type AppEnv string type DbProvider string +type TrustProxyConfig []string const ( // TracerName should be passed to otel.Tracer, trace.SpanFromContext when creating custom spans. @@ -42,18 +44,18 @@ type EnvConfigSchema struct { EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"` AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"` DbProvider DbProvider - DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` - TrustProxy bool `env:"TRUST_PROXY"` - TrustedPlatform string `env:"TRUSTED_PLATFORM"` - AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"` - AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` - AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` - AllowInsecureCallbackURLs bool `env:"ALLOW_INSECURE_CALLBACK_URLS"` - InternalAppURL string `env:"INTERNAL_APP_URL"` - UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"` - DisableRateLimiting bool `env:"DISABLE_RATE_LIMITING"` - VersionCheckDisabled bool `env:"VERSION_CHECK_DISABLED"` - StaticApiKey string `env:"STATIC_API_KEY" options:"file"` + DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` + TrustProxy TrustProxyConfig `env:"TRUST_PROXY"` + TrustedPlatform string `env:"TRUSTED_PLATFORM"` + AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"` + AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` + AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` + AllowInsecureCallbackURLs bool `env:"ALLOW_INSECURE_CALLBACK_URLS"` + InternalAppURL string `env:"INTERNAL_APP_URL"` + UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"` + DisableRateLimiting bool `env:"DISABLE_RATE_LIMITING"` + VersionCheckDisabled bool `env:"VERSION_CHECK_DISABLED"` + StaticApiKey string `env:"STATIC_API_KEY" options:"file"` FileBackend string `env:"FILE_BACKEND" options:"toLower"` UploadPath string `env:"UPLOAD_PATH"` @@ -392,3 +394,29 @@ func (a AppEnv) IsProduction() bool { func (a AppEnv) IsTest() bool { return a == AppEnvTest } + +func (config *TrustProxyConfig) UnmarshalText(text []byte) error { + value := strings.TrimSpace(string(text)) + + // Support boolean values for completely enabling or disabling trust proxy + enabled, err := strconv.ParseBool(value) + if err == nil { + if enabled { + *config = TrustProxyConfig{"0.0.0.0/0", "::/0"} + } else { + *config = nil + } + + return nil + } + + // Normalize and validate each explicit proxy before the server starts + proxies := strings.Split(value, ",") + for i, proxy := range proxies { + proxy = strings.TrimSpace(proxy) + proxies[i] = proxy + } + + *config = proxies + return nil +} diff --git a/backend/internal/common/env_config_test.go b/backend/internal/common/env_config_test.go index a28c13ce..2eabdbcc 100644 --- a/backend/internal/common/env_config_test.go +++ b/backend/internal/common/env_config_test.go @@ -124,11 +124,33 @@ func TestParseEnvConfig(t *testing.T) { err := parseAndValidateEnvConfig(t) require.NoError(t, err) assert.True(t, EnvConfig.UiConfigDisabled) - assert.True(t, EnvConfig.TrustProxy) + assert.Equal(t, TrustProxyConfig{"0.0.0.0/0", "::/0"}, EnvConfig.TrustProxy) assert.False(t, EnvConfig.AnalyticsDisabled) assert.False(t, EnvConfig.AllowInsecureCallbackURLs) }) + t.Run("should parse trusted proxy IP addresses and CIDR ranges", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("TRUST_PROXY", "10.0.0.0/8, 192.168.1.10, ::1/128") + + err := parseAndValidateEnvConfig(t) + require.NoError(t, err) + assert.Equal(t, TrustProxyConfig{"10.0.0.0/8", "192.168.1.10", "::1/128"}, EnvConfig.TrustProxy) + }) + + t.Run("should disable trusted proxies when set to false", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("TRUST_PROXY", "false") + + err := parseAndValidateEnvConfig(t) + require.NoError(t, err) + assert.Nil(t, EnvConfig.TrustProxy) + }) + + t.Run("should allow insecure callback URLs by default", func(t *testing.T) { + assert.True(t, defaultConfig().AllowInsecureCallbackURLs) + }) + t.Run("should default audit log retention days to 90", func(t *testing.T) { EnvConfig = defaultConfig() t.Setenv("DB_PROVIDER", "sqlite")