diff --git a/Gopkg.lock b/Gopkg.lock index 5a3758a17..d15441c37 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -19,6 +19,16 @@ revision = "aabad6e819789e569bd6aabf444c935aa9ba1e44" version = "v0.5.0" +[[projects]] + name = "github.com/bitly/oauth2_proxy" + packages = [ + "api", + "cookie", + "providers" + ] + revision = "b90a23473f10c7bb2d84acd033f7d7ed81b95dd3" + version = "v2.2" + [[projects]] branch = "v2" name = "github.com/coreos/go-oidc" @@ -31,6 +41,12 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "836bfd95fecc0f1511dd66bdbf2b5b61ab8b00b6" + version = "v1.2.11" + [[projects]] branch = "master" name = "github.com/golang/protobuf" @@ -70,6 +86,12 @@ revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" version = "v1.1.4" +[[projects]] + branch = "master" + name = "github.com/unrolled/secure" + packages = ["."] + revision = "8287f3899c8e3d490748e18fe7d438629132914e" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -130,12 +152,6 @@ revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" -[[projects]] - name = "gopkg.in/fsnotify.v1" - packages = ["."] - revision = "836bfd95fecc0f1511dd66bdbf2b5b61ab8b00b6" - version = "v1.2.11" - [[projects]] name = "gopkg.in/square/go-jose.v2" packages = [ @@ -149,6 +165,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "b502c41a61115d14d6379be26b0300f65d173bdad852f0170d387ebf2d7ec173" + inputs-digest = "f9ed0c5bfe9c08fe7aa500f3b8878e13494217d0b5cdbde213325fad2d544a4c" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index c4005e114..377d8c335 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -4,8 +4,8 @@ # [[constraint]] - name = "github.com/18F/hmacauth" - version = "~1.0.1" + name = "github.com/mbland/hmacauth" + version = "~1.0.2" [[constraint]] name = "github.com/BurntSushi/toml" @@ -36,7 +36,7 @@ name = "google.golang.org/api" [[constraint]] - name = "gopkg.in/fsnotify.v1" + name = "github.com/fsnotify/fsnotify" version = "~1.2.0" [[constraint]] diff --git a/main.go b/main.go index 287dc4894..cecad2afb 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,6 @@ import ( "log" "os" "runtime" - "strings" "time" "github.com/BurntSushi/toml" @@ -21,6 +20,8 @@ func main() { upstreams := StringArray{} skipAuthRegex := StringArray{} googleGroups := StringArray{} + httpAllowedHosts := StringArray{} + httpHostsProxyHeaders := StringArray{} config := flagSet.String("config", "", "path to config file") showVersion := flagSet.Bool("version", false, "print version string") @@ -81,6 +82,25 @@ func main() { flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") + // These are options that allow you to tune various parameters for https://github.com/unrolled/secure + flagSet.Var(&httpAllowedHosts, "httpAllowedHosts", "a list of fully qualified domain names that are allowed. Default is empty list, which allows any and all host names.") + flagSet.Var(&httpHostsProxyHeaders, "httpHostsProxyHeaders", "a set of header keys that may hold a proxied hostname value for the request.") + flagSet.Bool("httpSSLRedirect", false, "If set to true, then only allow HTTPS requests. Default is false.") + flagSet.Bool("httpSSLTemporaryRedirect", false, "If true, then a 302 will be used while redirecting. Default is false (301).") + flagSet.String("httpSSLHost", "", "the host name that is used to redirect HTTP requests to HTTPS. Default is \"\", which indicates to use the same host.") + flagSet.Int64("httpSTSSeconds", 0, "The max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header.") + flagSet.Bool("httpSTSIncludeSubdomains", false, "If set to true, the 'includeSubdomains' will be appended to the Strict-Transport-Security header. Default is false.") + flagSet.Bool("httpSTSPreload", false, "If set to true, the 'preload' flag will be appended to the Strict-Transport-Security header. Default is false.") + flagSet.Bool("httpForceSTSHeader", false, "STS header is only included when the connection is HTTPS. If you want to force it to always be added, set to true. Default is false.") + flagSet.Bool("httpFrameDeny", false, "If set to true, adds the X-Frame-Options header with the value of 'DENY'. Default is false.") + flagSet.String("httpCustomFrameOptionsValue", "", "allows the X-Frame-Options header value to be set with a custom value. This overrides the FrameDeny option. Default is \"\".") + flagSet.Bool("httpContentTypeNosniff", false, "If true, adds the X-Content-Type-Options header with the value 'nosniff'. Default is false.") + flagSet.Bool("httpBrowserXssFilter", false, "If true, adds the X-XSS-Protection header with the value '1; mode=block'. Default is false.") + flagSet.String("httpCustomBrowserXssValue", "", "Allows the X-XSS-Protection header value to be set with a custom value. This overrides the BrowserXssFilter option. Default is \"\".") + flagSet.String("httpContentSecurityPolicy", "", "Allows the Content-Security-Policy header value to be set with a custom value. Default is \"\". Passing a template string will replace '$NONCE' with a dynamic nonce value of 16 bytes for each request which can be later retrieved using the Nonce function.") + flagSet.String("httpPublicKey", "", "Implements HPKP to prevent MITM attacks with forged certificates. Default is \"\".") + flagSet.String("httpReferrerPolicy", "", "Allows the Referrer-Policy header with the value to be set with a custom value. Default is \"\".") + flagSet.Parse(os.Args[1:]) if *showVersion { @@ -99,31 +119,14 @@ func main() { } cfg.LoadEnvForStruct(opts) options.Resolve(opts, flagSet, cfg) - err := opts.Validate() if err != nil { log.Printf("%s", err) os.Exit(1) } - validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile) - oauthproxy := NewOAuthProxy(opts, validator) - - if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" { - if len(opts.EmailDomains) > 1 { - oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", ")) - } else if opts.EmailDomains[0] != "*" { - oauthproxy.SignInMessage = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0]) - } - } - if opts.HtpasswdFile != "" { - log.Printf("using htpasswd file %s", opts.HtpasswdFile) - oauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(opts.HtpasswdFile) - oauthproxy.DisplayHtpasswdForm = opts.DisplayHtpasswdForm - if err != nil { - log.Fatalf("FATAL: unable to open %s %s", opts.HtpasswdFile, err) - } - } + validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile) + oauthproxy := NewSecureProxy(opts, validator) s := &Server{ Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging, opts.RequestLoggingFormat), diff --git a/oauthproxy.go b/oauthproxy.go index 21e5dfc74..468f824a5 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -17,6 +17,7 @@ import ( "github.com/bitly/oauth2_proxy/cookie" "github.com/bitly/oauth2_proxy/providers" "github.com/mbland/hmacauth" + "github.com/unrolled/secure" ) const SignatureHeader = "GAP-Signature" @@ -115,6 +116,49 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) { return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath))) } +func NewSecureProxy(opts *Options, validator func(string) bool) http.Handler { + bareproxy := NewOAuthProxy(opts, validator) + var err error + + if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" { + if len(opts.EmailDomains) > 1 { + bareproxy.SignInMessage = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", ")) + } else if opts.EmailDomains[0] != "*" { + bareproxy.SignInMessage = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0]) + } + } + + if opts.HtpasswdFile != "" { + log.Printf("using htpasswd file %s", opts.HtpasswdFile) + bareproxy.HtpasswdFile, err = NewHtpasswdFromFile(opts.HtpasswdFile) + bareproxy.DisplayHtpasswdForm = opts.DisplayHtpasswdForm + if err != nil { + log.Fatalf("FATAL: unable to open %s %s", opts.HtpasswdFile, err) + } + } + + secureMiddleware := secure.New(secure.Options{ + AllowedHosts: opts.HttpAllowedHosts, + HostsProxyHeaders: opts.HttpHostsProxyHeaders, + SSLRedirect: opts.HttpSSLRedirect, + SSLTemporaryRedirect: opts.HttpSSLTemporaryRedirect, + SSLHost: opts.HttpSSLHost, + STSSeconds: opts.HttpSTSSeconds, + STSIncludeSubdomains: opts.HttpSTSIncludeSubdomains, + STSPreload: opts.HttpSTSPreload, + ForceSTSHeader: opts.HttpForceSTSHeader, + FrameDeny: opts.HttpFrameDeny, + CustomFrameOptionsValue: opts.HttpCustomFrameOptionsValue, + ContentTypeNosniff: opts.HttpContentTypeNosniff, + BrowserXssFilter: opts.HttpBrowserXssFilter, + CustomBrowserXssValue: opts.HttpCustomBrowserXssValue, + ContentSecurityPolicy: opts.HttpContentSecurityPolicy, + PublicKey: opts.HttpPublicKey, + ReferrerPolicy: opts.HttpReferrerPolicy, + }) + return secureMiddleware.Handler(bareproxy) +} + func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy { serveMux := http.NewServeMux() var auth hmacauth.HmacAuth diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 1e6b3140d..5416d01a8 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -836,3 +836,53 @@ func TestRequestSignaturePostRequest(t *testing.T) { assert.Equal(t, 200, st.rw.Code) assert.Equal(t, st.rw.Body.String(), "signatures match") } + +func TestHttpBrowserXssFilterTrue(t *testing.T) { + opts := NewOptions() + opts.ClientID = "bazquux" + opts.ClientSecret = "foobar" + opts.CookieSecret = "xyzzyplugh" + opts.EmailDomains = []string{"*"} + opts.HttpBrowserXssFilter = true + opts.Validate() + + proxy := NewSecureProxy(opts, func(string) bool { return true }) + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + proxy.ServeHTTP(rw, req) + assert.Equal(t, "1; mode=block", rw.HeaderMap.Get("X-XSS-Protection")) +} + +func TestHttpBrowserXssFilterFalseByDefault(t *testing.T) { + opts := NewOptions() + opts.ClientID = "bazquux" + opts.ClientSecret = "foobar" + opts.CookieSecret = "xyzzyplugh" + opts.EmailDomains = []string{"*"} + opts.Validate() + + proxy := NewSecureProxy(opts, func(string) bool { return true }) + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + proxy.ServeHTTP(rw, req) + assert.Equal(t, false, opts.HttpBrowserXssFilter) + assert.Equal(t, "", rw.HeaderMap.Get("X-XSS-Protection")) +} + +func TestSecureRobotsTxt(t *testing.T) { + opts := NewOptions() + opts.ClientID = "bazquux" + opts.ClientSecret = "foobar" + opts.CookieSecret = "xyzzyplugh" + opts.EmailDomains = []string{"*"} + opts.HttpBrowserXssFilter = true + opts.Validate() + + proxy := NewSecureProxy(opts, func(string) bool { return true }) + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/robots.txt", nil) + proxy.ServeHTTP(rw, req) + assert.Equal(t, 200, rw.Code) + assert.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String()) + assert.Equal(t, "1; mode=block", rw.HeaderMap.Get("X-XSS-Protection")) +} diff --git a/options.go b/options.go index 949fbba80..21042c1f0 100644 --- a/options.go +++ b/options.go @@ -79,6 +79,25 @@ type Options struct { SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"` + // These are options that allow you to tune various parameters for https://github.com/unrolled/secure + HttpAllowedHosts []string `flag:"httpAllowedHosts" cfg:"httpAllowedHosts"` + HttpHostsProxyHeaders []string `flag:"httpHostsProxyHeaders" cfg:"httpHostsProxyHeaders"` + HttpSSLRedirect bool `flag:"httpSSLRedirect" cfg:"httpSSLRedirect"` + HttpSSLTemporaryRedirect bool `flag:"httpSSLTemporaryRedirect" cfg:"httpSSLTemporaryRedirect"` + HttpSSLHost string `flag:"httpSSLHost" cfg:"httpSSLHost"` + HttpSTSSeconds int64 `flag:"httpSTSSeconds" cfg:"httpSTSSeconds"` + HttpSTSIncludeSubdomains bool `flag:"httpSTSIncludeSubdomains" cfg:"httpSTSIncludeSubdomains"` + HttpSTSPreload bool `flag:"httpSTSPreload" cfg:"httpSTSPreload"` + HttpForceSTSHeader bool `flag:"httpForceSTSHeader" cfg:"httpForceSTSHeader"` + HttpFrameDeny bool `flag:"httpFrameDeny" cfg:"httpFrameDeny"` + HttpCustomFrameOptionsValue string `flag:"httpCustomFrameOptionsValue" cfg:"httpCustomFrameOptionsValue"` + HttpContentTypeNosniff bool `flag:"httpContentTypeNosniff" cfg:"httpContentTypeNosniff"` + HttpBrowserXssFilter bool `flag:"httpBrowserXssFilter" cfg:"httpBrowserXssFilter"` + HttpCustomBrowserXssValue string `flag:"httpCustomBrowserXssValue" cfg:"httpCustomBrowserXssValue"` + HttpContentSecurityPolicy string `flag:"httpContentSecurityPolicy" cfg:"httpContentSecurityPolicy"` + HttpPublicKey string `flag:"httpPublicKey" cfg:"httpPublicKey"` + HttpReferrerPolicy string `flag:"httpReferrerPolicy" cfg:"httpReferrerPolicy"` + // internal values that are set after config validation redirectURL *url.URL proxyURLs []*url.URL diff --git a/test.sh b/test.sh index acc17a231..1a97e41f5 100755 --- a/test.sh +++ b/test.sh @@ -11,4 +11,4 @@ for pkg in $(go list ./... | grep -v '/vendor/' ); do echo "go test -v -race $pkg" GOMAXPROCS=4 go test -v -timeout 90s0s -race "$pkg" || EXIT_CODE=1 done -exit $EXIT_CODE \ No newline at end of file +exit $EXIT_CODE diff --git a/watcher.go b/watcher.go index bedb9f893..b739d1421 100644 --- a/watcher.go +++ b/watcher.go @@ -8,7 +8,7 @@ import ( "path/filepath" "time" - "gopkg.in/fsnotify.v1" + "github.com/fsnotify/fsnotify" ) func WaitForReplacement(filename string, op fsnotify.Op,