Skip to content

Add standard Forwarded header support #10322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/user-guide/nginx-configuration/configmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ The following table shows a configuration option's name, type, and the default v
|[enable-real-ip](#enable-real-ip)|bool|"false"||
|[forwarded-for-header](#forwarded-for-header)|string|"X-Forwarded-For"||
|[compute-full-forwarded-for](#compute-full-forwarded-for)|bool|"false"||
|[enable-forwarded-rfc7239](#enable-forwarded-rfc7239)|bool|"false"||
|[forwarded-rfc7239-strip-incomming](#forwarded-rfc7239-strip-incomming)|bool|"false"||
|[forwarded-rfc7239](#forwarded-rfc7239)|[]string|"for"||
|[forwarded-rfc7239-for](#forwarded-rfc7239-for)|string|"ip"||
|[forwarded-rfc7239-by](#forwarded-rfc7239-by)|string|"ip"||
|[proxy-add-original-uri-header](#proxy-add-original-uri-header)|bool|"false"||
|[generate-request-id](#generate-request-id)|bool|"true"||
|[enable-opentracing](#enable-opentracing)|bool|"false"||
Expand Down Expand Up @@ -922,6 +927,26 @@ Sets the header field for identifying the originating IP address of a client. _*

Append the remote address to the X-Forwarded-For header instead of replacing it. When this option is enabled, the upstream application is responsible for extracting the client IP based on its own list of trusted proxies.

## enable-forwarded-rfc7239

Enable standard Forwarded header defined in RFC 7239, or the Forwared header will not be sent to upstream and the incoming Forwarded header will be discarded. The parameters can be configured using [forwarded-rfc7239](#forwarded-rfc7239). Transition between Forwarded header and X-Forwarded headers will not happen. _**default:**_ false

## forwarded-rfc7239-strip-incomming

Whether or not retain Forwarded header from downstream. _**default:**_ false

## forwarded-rfc7239

Sets enabled parameters and their order. Supported parameters are "for", "by", "host" and, "proto". _**default:**_ for

## forwarded-rfc7239-for

Sets value of "for" parameter. It can be "ip" or static obfuscated string. If "ip" is given, the remote client address will be used. The static obfuscated string is selected by user and must start with a underscore "_" and consist of only digits, letters and characters ".", "_", and "-". _**default:**_ ip

## forwarded-rfc7239-by

Sets value of "by" parameter. It can be "ip" or static obfuscated string. If "ip" is given, the server's host-port pair that remote client connects to will be used. The static obfuscated string is selected by user and must start with a underscore "_" and consist of only digits, letters and characters ".", "_", and "-". _**default:**_ ip

## proxy-add-original-uri-header

Adds an X-Original-Uri header with the original request URI to the backend request
Expand Down
25 changes: 25 additions & 0 deletions internal/ingress/controller/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,26 @@ type Configuration struct {
// Default: false
ComputeFullForwardedFor bool `json:"compute-full-forwarded-for,omitempty"`

// Enable standard forwarded header.
// Default: false
EnableForwardedRFC7239 bool `json:"enable-forwarded-rfc7239"`

// Sets if strips incoming Forwarded header.
// Default: false
ForwardedRFC7239StripIncomming bool `json:"forwarded-rfc7239-strip-incomming"`

// Sets Forwarded parameters and their order. Available options are "for", "by", "host", "proto".
// Default: "for"
ForwardedRFC7239 []string `json:"forwarded-rfc7239"`

// Sets Forwarded "for" parameter node identifier, should be "ip" or a static obfuscated string.
// Default: "ip"
ForwardedRFC7239For string `json:"forwarded-rfc7239-for,omitempty"`

// Sets Forwarded "by" parameter node identifier, should be "ip" or a static obfuscated string.
// Default: "ip"
ForwardedRFC7239By string `json:"forwarded-rfc7239-by,omitempty"`

// If the request does not have a request-id, should we generate a random value?
// Default: true
GenerateRequestID bool `json:"generate-request-id,omitempty"`
Expand Down Expand Up @@ -889,6 +909,11 @@ func NewDefault() Configuration {
EnableRealIp: false,
ForwardedForHeader: "X-Forwarded-For",
ComputeFullForwardedFor: false,
EnableForwardedRFC7239: false,
ForwardedRFC7239StripIncomming: false,
ForwardedRFC7239: []string{"for"},
ForwardedRFC7239For: "ip",
ForwardedRFC7239By: "ip",
ProxyAddOriginalURIHeader: false,
GenerateRequestID: true,
HTTP2MaxFieldSize: "",
Expand Down
50 changes: 50 additions & 0 deletions internal/ingress/controller/template/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ const (
nginxStatusIpv4Whitelist = "nginx-status-ipv4-whitelist"
nginxStatusIpv6Whitelist = "nginx-status-ipv6-whitelist"
proxyHeaderTimeout = "proxy-protocol-header-timeout"
forwardedRFC7239 = "forwarded-rfc7239"
forwardedRFC7239For = "forwarded-rfc7239-for"
forwardedRFC7239By = "forwarded-rfc7239-by"
workerProcesses = "worker-processes"
globalAuthURL = "global-auth-url"
globalAuthMethod = "global-auth-method"
Expand Down Expand Up @@ -83,6 +86,7 @@ var (
"global_throttle_cache": 10240,
}
defaultGlobalAuthRedirectParam = "rd"
forwardedRFC7239ValidParams = [...]string{"for", "by", "host", "proto"}
)

const (
Expand Down Expand Up @@ -346,6 +350,36 @@ func ReadConfig(src map[string]string) config.Configuration {
}
}

if val, ok := conf[forwardedRFC7239]; ok {
delete(conf, forwardedRFC7239)
params := []string{} // non-nil value
for _, param := range splitAndTrimSpace(val, ",") {
param = strings.ToLower(param)
if sliceContains(forwardedRFC7239ValidParams[:], param) {
if sliceContains(params, param) {
klog.Warningf("%v ignores duplicate parameter: %v", forwardedRFC7239, param)
continue
}
params = append(params, param)
}
}
to.ForwardedRFC7239 = params
}

if val, ok := conf[forwardedRFC7239For]; ok {
if !isValidObfuscatedIdentifier(val) {
delete(conf, forwardedRFC7239For)
klog.Warningf("%v is not a valid obfuscated identifier", val)
}
}

if val, ok := conf[forwardedRFC7239By]; ok {
if !isValidObfuscatedIdentifier(val) {
delete(conf, forwardedRFC7239By)
klog.Warningf("%v is not a valid obfuscated identifier", val)
}
}

streamResponses := 1
if val, ok := conf[proxyStreamResponses]; ok {
delete(conf, proxyStreamResponses)
Expand Down Expand Up @@ -487,3 +521,19 @@ func dictKbToStr(size int) string {
}
return fmt.Sprintf("%dK", size)
}

func sliceContains(slice []string, s string) bool {
for _, value := range slice {
if value == s {
return true
}
}
return false
}

// isValidObfuscatedIdentifier tests if s is a valid obfuscated identifier
// as section 6.3 of rfc7239 defines.
func isValidObfuscatedIdentifier(s string) bool {
re := regexp.MustCompile("^_[0-9a-zA-Z._-]*$")
return re.MatchString(s)
}
86 changes: 86 additions & 0 deletions internal/ingress/controller/template/configmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,38 @@ func TestGlobalExternalAuthCacheDurationParsing(t *testing.T) {
}
}

func TestForwardedRFC7239Parsing(t *testing.T) {
testCases := map[string]struct {
params string
expect []string
}{
"nothing": {"", []string{}},
"spaces": {" ", []string{}},
"one param": {"for", []string{"for"}},
"two params": {"by,for", []string{"by", "for"}},
"case insensitive": {"for,HOST,By", []string{"for", "host", "by"}},
"duplicate params": {"for,host,for,by", []string{"for", "host", "by"}},
}

{
name := "absent field"
expect := []string{"for"} // default
cfg := ReadConfig(map[string]string{})

if !reflect.DeepEqual(cfg.ForwardedRFC7239, expect) {
t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", name, expect, cfg.ForwardedRFC7239)
}
}

for name, tc := range testCases {
cfg := ReadConfig(map[string]string{"forwarded-rfc7239": tc.params})

if !reflect.DeepEqual(cfg.ForwardedRFC7239, tc.expect) {
t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", name, tc.expect, cfg.ForwardedRFC7239)
}
}
}

func TestLuaSharedDictsParsing(t *testing.T) {
testsCases := []struct {
name string
Expand Down Expand Up @@ -530,3 +562,57 @@ func TestDictKbToStr(t *testing.T) {
}
}
}

func TestSliceContains(t *testing.T) {
testCases := map[string]struct {
slice []string
find string
expect bool
}{
"containing": {
slice: []string{"ip", "_obf"},
find: "ip",
expect: true,
},
"not containing": {
slice: []string{"ip", "_obf"},
find: "obfuscated",
expect: false,
},
}

for name, tc := range testCases {
if b := sliceContains(tc.slice, tc.find); b != tc.expect {
t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", name, tc.expect, b)
}
}
}

func TestIsValidObfuscatedIdentifier(t *testing.T) {
testCases := map[string]struct {
input string
expect bool
}{
"rfc example _hidden": {
input: "_hidden",
expect: true,
},
"rfc example _SEVKISEK": {
input: "_SEVKISEK",
expect: true,
},
"non-leading underscore": {
input: "hidden",
expect: false,
},
"containing invalid characters": {
input: "_hidden:",
expect: false,
},
}
for name, tc := range testCases {
if isValid := isValidObfuscatedIdentifier(tc.input); isValid != tc.expect {
t.Errorf("Testing %v. Expected \"%v\" but \"%v\" was returned", name, tc.input, isValid)
}
}
}
Loading