Gate forwarded-header trust behind trusted-proxy config (closes #88)
All checks were successful
check / check (push) Successful in 6s
All checks were successful
check / check (push) Successful in 6s
All three rate limiters (receiver, login, password change) now key on the connection's own address unless the direct peer is inside the new TRUSTED_PROXIES CIDR list, in which case X-Forwarded-For is walked right to left for the first non-proxy hop. Default is the empty list, which trusts nothing. A set-but-unparseable value aborts startup.
This commit was merged in pull request #122.
This commit is contained in:
@@ -20,6 +20,10 @@ const (
|
||||
caseUnsetUsesDefault = "unset uses default"
|
||||
caseValidValueParsed = "valid value is parsed"
|
||||
caseUnparseableFails = "unparseable value fails startup"
|
||||
|
||||
// cidrPrivateV4 is the sample trusted-proxy block the
|
||||
// TRUSTED_PROXIES cases are built from.
|
||||
cidrPrivateV4 = "10.0.0.0/8"
|
||||
)
|
||||
|
||||
func TestEnvironmentConfig(t *testing.T) {
|
||||
@@ -179,9 +183,10 @@ func TestRetentionSweepInterval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// expectStartupError asserts that fx refuses to build the app,
|
||||
// which is what a set-but-invalid environment value must cause.
|
||||
func expectStartupError(t *testing.T) {
|
||||
// startupError builds the app config.New belongs to and returns
|
||||
// the error fx reports, which is non-nil whenever an environment
|
||||
// value is set but invalid.
|
||||
func startupError(t *testing.T) error {
|
||||
t.Helper()
|
||||
|
||||
var cfg *config.Config
|
||||
@@ -196,7 +201,33 @@ func expectStartupError(t *testing.T) {
|
||||
fx.Populate(&cfg),
|
||||
)
|
||||
|
||||
assert.Error(t, app.Err())
|
||||
return app.Err()
|
||||
}
|
||||
|
||||
// expectStartupError asserts that fx refuses to build the app,
|
||||
// which is what a set-but-invalid environment value must cause.
|
||||
func expectStartupError(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
assert.Error(t, startupError(t))
|
||||
}
|
||||
|
||||
// expectStartupErrorFor asserts that startup fails, that the error
|
||||
// names the offending variable so an operator can find it, and,
|
||||
// when sentinel is non-nil, that it wraps that sentinel.
|
||||
func expectStartupErrorFor(
|
||||
t *testing.T,
|
||||
key string,
|
||||
sentinel error,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
err := startupError(t)
|
||||
require.ErrorContains(t, err, key)
|
||||
|
||||
if sentinel != nil {
|
||||
require.ErrorIs(t, err, sentinel)
|
||||
}
|
||||
}
|
||||
|
||||
func testRetentionSweepIntervalSuccess(
|
||||
@@ -351,7 +382,11 @@ func TestReceiverRateLimit(t *testing.T) {
|
||||
set bool
|
||||
value string
|
||||
expectError bool
|
||||
expected int
|
||||
// sentinel, when set, must be wrapped by the startup
|
||||
// error; every error case must additionally name the
|
||||
// variable in its message.
|
||||
sentinel error
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: caseUnsetUsesDefault,
|
||||
@@ -375,12 +410,14 @@ func TestReceiverRateLimit(t *testing.T) {
|
||||
set: true,
|
||||
value: "0",
|
||||
expectError: true,
|
||||
sentinel: config.ErrNonPositiveValue,
|
||||
},
|
||||
{
|
||||
name: "negative fails startup",
|
||||
set: true,
|
||||
value: "-5",
|
||||
expectError: true,
|
||||
sentinel: config.ErrNonPositiveValue,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -399,7 +436,9 @@ func TestReceiverRateLimit(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.expectError {
|
||||
expectStartupError(t)
|
||||
expectStartupErrorFor(
|
||||
t, "RECEIVER_RATE_LIMIT", tt.sentinel,
|
||||
)
|
||||
} else {
|
||||
testReceiverRateLimitSuccess(t, tt.expected)
|
||||
}
|
||||
@@ -432,3 +471,116 @@ func testReceiverRateLimitSuccess(
|
||||
|
||||
assert.Equal(t, expected, cfg.ReceiverRateLimit)
|
||||
}
|
||||
|
||||
func TestTrustedProxies(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
set bool
|
||||
value string
|
||||
expectError bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
// The default must be "trust nobody": an empty list
|
||||
// means forwarded headers are ignored, never that
|
||||
// every peer may speak for the client.
|
||||
name: caseUnsetUsesDefault,
|
||||
set: false,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "blank value trusts nothing",
|
||||
set: true,
|
||||
value: " ",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: caseValidValueParsed,
|
||||
set: true,
|
||||
value: cidrPrivateV4 + ", 192.168.1.7 ,2001:db8::/32",
|
||||
expected: []string{
|
||||
cidrPrivateV4, "192.168.1.7/32", "2001:db8::/32",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "host bits are masked off",
|
||||
set: true,
|
||||
value: "10.1.2.3/8",
|
||||
expected: []string{cidrPrivateV4},
|
||||
},
|
||||
{
|
||||
// Peer addresses are unmapped before they are
|
||||
// matched, so an IPv4-mapped prefix kept in that
|
||||
// form could never match anything.
|
||||
name: "IPv4-mapped prefix is unmapped",
|
||||
set: true,
|
||||
value: "::ffff:10.0.0.0/104",
|
||||
expected: []string{cidrPrivateV4},
|
||||
},
|
||||
{
|
||||
name: caseUnparseableFails,
|
||||
set: true,
|
||||
value: cidrPrivateV4 + ",not-an-address",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "out-of-range prefix length fails startup",
|
||||
set: true,
|
||||
value: "10.0.0.0/33",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Cannot use t.Parallel() here because t.Setenv
|
||||
// is incompatible with parallel subtests.
|
||||
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
|
||||
|
||||
if tt.set {
|
||||
t.Setenv("TRUSTED_PROXIES", tt.value)
|
||||
} else {
|
||||
require.NoError(t, os.Unsetenv("TRUSTED_PROXIES"))
|
||||
}
|
||||
|
||||
if tt.expectError {
|
||||
expectStartupErrorFor(
|
||||
t, "TRUSTED_PROXIES", config.ErrInvalidCIDR,
|
||||
)
|
||||
} else {
|
||||
testTrustedProxiesSuccess(t, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testTrustedProxiesSuccess(
|
||||
t *testing.T,
|
||||
expected []string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
var cfg *config.Config
|
||||
|
||||
app := fxtest.New(
|
||||
t,
|
||||
fx.Provide(
|
||||
globals.New,
|
||||
logger.New,
|
||||
config.New,
|
||||
),
|
||||
fx.Populate(&cfg),
|
||||
)
|
||||
require.NoError(t, app.Err())
|
||||
|
||||
app.RequireStart()
|
||||
|
||||
defer app.RequireStop()
|
||||
|
||||
got := make([]string, 0, len(cfg.TrustedProxies))
|
||||
for _, prefix := range cfg.TrustedProxies {
|
||||
got = append(got, prefix.String())
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user