diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 495b693d..ae82c2d3 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -30,6 +30,16 @@ func (app *BootstrapApp) setupRouter() error { if err != nil { return fmt.Errorf("failed to set trusted proxies: %w", err) } + + app.runtime.TrustedProxiesConfigured = true + } else { + err := engine.SetTrustedProxies(nil) + + if err != nil { + return fmt.Errorf("failed to set trusted proxies: %w", err) + } + + app.log.App.Warn().Msg("Trusted proxies are not configured, IP access controls will NOT work") } middlewareProvideFor := []any{ diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index ffafaffd..cc398d1d 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -111,9 +111,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { clientIP := c.ClientIP() aclsCtx := &service.ACLContext{ - ACLs: acls, - IP: net.ParseIP(clientIP), - Path: proxyCtx.Path, + ACLs: acls, + IP: net.ParseIP(clientIP), + Path: proxyCtx.Path, + TrustedProxiesConfigured: controller.runtime.TrustedProxiesConfigured, } if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) { diff --git a/internal/model/runtime.go b/internal/model/runtime.go index e1c034d3..92000265 100644 --- a/internal/model/runtime.go +++ b/internal/model/runtime.go @@ -1,17 +1,18 @@ package model type RuntimeConfig struct { - AppURL string - UUID string - CookieDomain string - SessionCookieName string - CSRFCookieName string - RedirectCookieName string - OAuthSessionCookieName string - LocalUsers []LocalUser - OAuthProviders map[string]OAuthServiceConfig - OAuthWhitelist []string - ConfiguredProviders []Provider + AppURL string + UUID string + CookieDomain string + SessionCookieName string + CSRFCookieName string + RedirectCookieName string + OAuthSessionCookieName string + LocalUsers []LocalUser + OAuthProviders map[string]OAuthServiceConfig + OAuthWhitelist []string + ConfiguredProviders []Provider + TrustedProxiesConfigured bool } type Provider struct { diff --git a/internal/service/access_controls_rules.go b/internal/service/access_controls_rules.go index 98d2f1df..f0597a31 100644 --- a/internal/service/access_controls_rules.go +++ b/internal/service/access_controls_rules.go @@ -215,6 +215,10 @@ type IPAllowedRule struct { } func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect { + if !ctx.TrustedProxiesConfigured { + return EffectAllow // We can't block the proxy + } + // merge global and per-app block/allow lists blockedIps := append([]string{}, rule.Config.Auth.IP.Block...) allowedIPs := append([]string{}, rule.Config.Auth.IP.Allow...) @@ -263,6 +267,10 @@ type IPBypassedRule struct { } func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect { + if !ctx.TrustedProxiesConfigured { + return EffectDeny + } + // merge global and per-app bypass lists bypassList := append([]string{}, rule.Config.Auth.IP.Bypass...) if ctx.ACLs != nil { diff --git a/internal/service/access_controls_rules_test.go b/internal/service/access_controls_rules_test.go index 2fc43f51..f3371801 100644 --- a/internal/service/access_controls_rules_test.go +++ b/internal/service/access_controls_rules_test.go @@ -611,11 +611,20 @@ func TestIPAllowedRule(t *testing.T) { ctx *ACLContext expected Effect }{ + { + name: "when trusted proxies are not configured, IP is allowed", + ctx: &ACLContext{ + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + }, + expected: EffectAllow, + }, { name: "allows when ACLs are nil and no global lists configured", ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("10.0.0.1"), + ACLs: nil, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -625,7 +634,8 @@ func TestIPAllowedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Block: []string{"10.0.0.1"}}, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -637,8 +647,9 @@ func TestIPAllowedRule(t *testing.T) { }, }, ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("10.0.0.5"), + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -648,7 +659,8 @@ func TestIPAllowedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Allow: []string{"192.168.1.0/24"}}, }, - IP: net.ParseIP("192.168.1.10"), + IP: net.ParseIP("192.168.1.10"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -660,8 +672,9 @@ func TestIPAllowedRule(t *testing.T) { }, }, ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("192.168.1.10"), + ACLs: &model.App{}, + IP: net.ParseIP("192.168.1.10"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -671,15 +684,17 @@ func TestIPAllowedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Allow: []string{"192.168.1.0/24"}}, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, { name: "allows when no block or allow lists are configured", ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("10.0.0.1"), + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -692,7 +707,8 @@ func TestIPAllowedRule(t *testing.T) { Allow: []string{"10.0.0.1"}, }, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -705,7 +721,8 @@ func TestIPAllowedRule(t *testing.T) { Allow: []string{"10.0.0.1"}, }, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -735,12 +752,23 @@ func TestIPBypassedRule(t *testing.T) { ctx *ACLContext expected Effect }{ + { + name: "when trusted proxies are not configured, IP is not bypassed", + rule: defaultIPBR, + ctx: &ACLContext{ + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: false, + }, + expected: EffectDeny, + }, { name: "deny when ACLs are nil and no global bypass", rule: defaultIPBR, ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("10.0.0.1"), + ACLs: nil, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -748,8 +776,9 @@ func TestIPBypassedRule(t *testing.T) { name: "allows when ACLs are nil but IP matches global bypass", rule: globBypassIPBR, ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("10.0.0.5"), + ACLs: nil, + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -757,8 +786,9 @@ func TestIPBypassedRule(t *testing.T) { name: "denies when ACLs are nil and IP does not match global bypass", rule: globBypassIPBR, ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("192.168.1.1"), + ACLs: nil, + IP: net.ParseIP("192.168.1.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -769,7 +799,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, }, - IP: net.ParseIP("10.0.0.5"), + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -780,7 +811,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"172.16.0.0/24"}}, }, - IP: net.ParseIP("10.0.0.5"), + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -791,7 +823,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, }, - IP: net.ParseIP("10.0.0.5"), + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -802,7 +835,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, }, - IP: net.ParseIP("192.168.1.1"), + IP: net.ParseIP("192.168.1.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -810,8 +844,9 @@ func TestIPBypassedRule(t *testing.T) { name: "denies when bypass list is empty", rule: defaultIPBR, ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("10.0.0.1"), + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -822,7 +857,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}}, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, diff --git a/internal/service/policy_engine.go b/internal/service/policy_engine.go index c3bbb133..943cd4c9 100644 --- a/internal/service/policy_engine.go +++ b/internal/service/policy_engine.go @@ -29,10 +29,11 @@ type Rule interface { } type ACLContext struct { - ACLs *model.App - UserContext *model.UserContext - IP net.IP - Path string + ACLs *model.App + UserContext *model.UserContext + IP net.IP + Path string + TrustedProxiesConfigured bool } type PolicyEngine struct { diff --git a/internal/test/test.go b/internal/test/test.go index a3f07ca0..20275adc 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -163,9 +163,10 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) { }, }, }, - CookieDomain: "example.com", - AppURL: "https://tinyauth.example.com", - SessionCookieName: "tinyauth-session", + CookieDomain: "example.com", + AppURL: "https://tinyauth.example.com", + SessionCookieName: "tinyauth-session", + TrustedProxiesConfigured: true, } return config, runtime