From a55f856df7c9a638d3dbe9f4d978e0154ae15b54 Mon Sep 17 00:00:00 2001 From: Pierce Lopez Date: Sat, 23 May 2020 23:59:14 -0400 Subject: [PATCH] new option real-client-ip-header for alternatives to X-Real-IP also remove the recently-added --xheaders option (it was never in a release), in favor of disabling trust of the X-Real-IP header by setting the new real-client-ip-header option to a blank/empty string inspired by https://github.com/oauth2-proxy/oauth2-proxy/pull/503 --- README.md | 1 + contrib/oauth2_proxy.cfg.example | 4 ++-- http.go | 14 ++++++++++++++ logging_handler.go | 11 ++++++----- main.go | 3 ++- oauthproxy.go | 10 ++++++---- options.go | 21 +++++++++++++++++++-- 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 068352618..e16c0262a 100644 --- a/README.md +++ b/README.md @@ -306,6 +306,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) 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/main.go b/main.go index 38623bfe9..37e34ffe0 100644 --- a/main.go +++ b/main.go @@ -74,6 +74,7 @@ func mainFlagSet() *flag.FlagSet { 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)") @@ -150,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 4d1e5b094..efadfe3a7 100644 --- a/options.go +++ b/options.go @@ -82,9 +82,9 @@ type Options struct { Scope string `flag:"scope" cfg:"scope"` ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` - 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"` @@ -120,9 +120,9 @@ func NewOptions() *Options { PassAccessToken: false, PassHostHeader: true, ApprovalPrompt: "force", - XHeaders: true, RequestLogging: true, RequestLoggingFormat: defaultRequestLoggingFormat, + RealClientIPHeader: "X-Real-IP", } } @@ -223,6 +223,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 "))