diff --git a/README.md b/README.md index 973eac361..1269d2b7d 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,7 @@ Usage of oauth2_proxy: -provider string: OAuth provider (default "google") -proxy-prefix string: the url root path that this proxy should be nested under (e.g. //sign_in) (default "/oauth2") -proxy-websockets: enables WebSocket proxying (default true) + -real-client-ip-header: HTTP header indicating the actual ip address of the client (blank to disable) (default "X-Real-IP") -redeem-url string: Token redemption endpoint -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" -request-logging: Log requests to stdout (default true) @@ -326,7 +327,6 @@ Usage of oauth2_proxy: -validate-url string: Access token validation endpoint -version: print version string -whitelist-domain value: allowed domain for redirection after authentication, leading '.' allows subdomains (may be given multiple times) - -xheaders: Trust X-Real-IP request header (appropriate when behind a reverse proxy) (default true) ``` diff --git a/contrib/oauth2_proxy.cfg.example b/contrib/oauth2_proxy.cfg.example index d24c1c379..15d64f0b3 100644 --- a/contrib/oauth2_proxy.cfg.example +++ b/contrib/oauth2_proxy.cfg.example @@ -9,9 +9,9 @@ # tls_cert_file = "" # tls_key_file = "" -## whether to trust the X-Real-IP request header for logging +## can be set to "X-Real-IP" (default), "X-Forwarded-For", "X-ProxyUser-IP", or "" (disabled) ## disable if not running oauth2_proxy behind another reverse-proxy or load-balancer -# xheaders = true +# real_client_ip_header = "X-Real-IP" ## the OAuth Redirect URL ## defaults to "https://" + requested host header + "/oauth2/callback" diff --git a/http.go b/http.go index aa764c842..d15d3bf00 100644 --- a/http.go +++ b/http.go @@ -108,3 +108,17 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc.SetKeepAlivePeriod(3 * time.Minute) return tc, nil } + +// extract client ip address from configured header +func extractClientIP(req *http.Request, header string) string { + if header != "" { + val := req.Header.Get(header) + if val != "" { + if header == "X-Forwarded-For" { + val = strings.TrimSpace(strings.SplitN(val, ",", 2)[0]) + } + return val + } + } + return "" +} diff --git a/logging_handler.go b/logging_handler.go index ab555a092..8c33e5f87 100644 --- a/logging_handler.go +++ b/logging_handler.go @@ -107,16 +107,16 @@ type loggingHandler struct { writer io.Writer handler http.Handler enabled bool - xheaders bool + ipHeader string logTemplate *template.Template } -func LoggingHandler(out io.Writer, h http.Handler, enabled bool, xheaders bool, requestLoggingTpl string) http.Handler { +func LoggingHandler(out io.Writer, h http.Handler, enabled bool, ipHeader, requestLoggingTpl string) http.Handler { return loggingHandler{ writer: out, handler: h, enabled: enabled, - xheaders: xheaders, + ipHeader: ipHeader, logTemplate: template.Must(template.New("request-log").Parse(requestLoggingTpl + "\n")), } } @@ -149,8 +149,9 @@ func (h loggingHandler) writeLogLine(username, upstream string, req *http.Reques } client := req.RemoteAddr - if h.xheaders && req.Header.Get("X-Real-IP") != "" { - client = req.Header.Get("X-Real-IP") + hval := extractClientIP(req, h.ipHeader) + if hval != "" { + client = hval } if c, _, err := net.SplitHostPort(client); err == nil { diff --git a/logging_handler_test.go b/logging_handler_test.go index ba8a0e31d..94e40c51c 100644 --- a/logging_handler_test.go +++ b/logging_handler_test.go @@ -34,7 +34,7 @@ func TestLoggingHandler_ServeHTTP(t *testing.T) { w.Write([]byte("test")) } - h := LoggingHandler(buf, http.HandlerFunc(handler), true, true, test.Format) + h := LoggingHandler(buf, http.HandlerFunc(handler), true, "", test.Format) r, _ := http.NewRequest("GET", "/foo/bar", nil) r.RemoteAddr = "127.0.0.1" r.Host = "test-server" diff --git a/main.go b/main.go index cc9de6f36..fd6661b71 100644 --- a/main.go +++ b/main.go @@ -71,9 +71,9 @@ func mainFlagSet() *flag.FlagSet { flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") - flagSet.Bool("xheaders", true, "Trust X-Real-IP request header (appropriate when behind a reverse proxy)") flagSet.Bool("request-logging", true, "Log requests to stdout") flagSet.String("request-logging-format", defaultRequestLoggingFormat, "Template for request log lines") + flagSet.String("real-client-ip-header", "X-Real-IP", "HTTP header indicating the actual ip address of the client (blank to disable)") flagSet.String("provider", "google", "OAuth provider") flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (e.g. https://accounts.google.com)") @@ -151,7 +151,7 @@ func main() { } s := &Server{ - Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging, opts.XHeaders, opts.RequestLoggingFormat), + Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging, opts.RealClientIPHeader, opts.RequestLoggingFormat), Opts: opts, } s.ListenAndServe() diff --git a/oauthproxy.go b/oauthproxy.go index cb9044a9e..d38e36b87 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -70,7 +70,7 @@ type OAuthProxy struct { PassUserHeaders bool BasicAuthPassword string PassAccessToken bool - XHeaders bool + ClientIPHeader string CookieCipher *cookie.Cipher skipAuthRegex []string skipAuthPreflight bool @@ -237,7 +237,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { BasicAuthPassword: opts.BasicAuthPassword, PassAccessToken: opts.PassAccessToken, SkipProviderButton: opts.SkipProviderButton, - XHeaders: opts.XHeaders, + ClientIPHeader: opts.RealClientIPHeader, CookieCipher: cipher, templates: loadTemplates(opts.CustomTemplatesDir), Footer: opts.Footer, @@ -521,8 +521,10 @@ func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) { func (p *OAuthProxy) getRemoteAddr(req *http.Request) (s string) { s = req.RemoteAddr - if p.XHeaders && req.Header.Get("X-Real-IP") != "" { - s += fmt.Sprintf(" (%q)", req.Header.Get("X-Real-IP")) + + hval := extractClientIP(req, p.ClientIPHeader) + if hval != "" { + s += fmt.Sprintf(" (%q)", hval) } return } diff --git a/options.go b/options.go index 310bab654..ec925d80e 100644 --- a/options.go +++ b/options.go @@ -83,9 +83,9 @@ type Options struct { Prompt string `flag:"prompt" cfg:"prompt"` ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0 - XHeaders bool `flag:"xheaders" cfg:"xheaders"` RequestLogging bool `flag:"request-logging" cfg:"request_logging"` RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format"` + RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"` SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"` @@ -122,9 +122,9 @@ func NewOptions() *Options { PassHostHeader: true, Prompt: "", // Change to "login" when ApprovalPrompt deprecated/removed ApprovalPrompt: "force", - XHeaders: true, RequestLogging: true, RequestLoggingFormat: defaultRequestLoggingFormat, + RealClientIPHeader: "X-Real-IP", } } @@ -225,6 +225,23 @@ func (o *Options) Validate() error { msgs = parseSignatureKey(o, msgs) msgs = validateCookieName(o, msgs) + if o.RealClientIPHeader != "" { + valid := false + realClientIPHeaders := []string{ + "X-Real-IP", + "X-Forwarded-For", + "X-ProxyUser-IP", + } + for _, s := range realClientIPHeaders { + if o.RealClientIPHeader == s { + valid = true + } + } + if !valid { + msgs = append(msgs, fmt.Sprintf("unsupported real-client-ip-header %q", o.RealClientIPHeader)) + } + } + if len(msgs) != 0 { return fmt.Errorf("Invalid configuration:\n %s", strings.Join(msgs, "\n "))