From 3a96e6d7e7753162c13f6edf067583145c1bde0b Mon Sep 17 00:00:00 2001 From: Lenin Alevski Date: Mon, 6 Apr 2020 13:24:15 -0700 Subject: [PATCH] Secure Middleware (#37) adding secure middleware to enforce security headers, most of the options can be configured via env variables adding prefix for mcs env variables adding http redirect to https, adding csp report only, etc solving conflicts passing tls port configured by cli to secure middleware update go.sum adding default port, tlsport, host and tlshostname fix tlsport bug --- .gitignore | 4 ++ cmd/mcs/main.go | 2 +- cmd/mcs/server.go | 57 +++++++++++++-- go.mod | 2 + go.sum | 9 +++ restapi/client-admin.go | 2 +- restapi/config.go | 146 +++++++++++++++++++++++++++++++++++++++ restapi/configure_mcs.go | 30 +++++++- restapi/consts.go | 33 +++++++-- 9 files changed, 272 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 976d6f065..ab873a055 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ mcs !mcs/ dist/ + +# Ignore tls cert and key +private.key +public.crt diff --git a/cmd/mcs/main.go b/cmd/mcs/main.go index 89d8f8585..46d533f48 100644 --- a/cmd/mcs/main.go +++ b/cmd/mcs/main.go @@ -58,7 +58,7 @@ var appCmds = []cli.Command{ func newApp(name string) *cli.App { // Collection of m3 commands currently supported are. - commands := []cli.Command{} + var commands []cli.Command // Collection of m3 commands currently supported in a trie tree. commandsTree := trie.NewTrie() diff --git a/cmd/mcs/server.go b/cmd/mcs/server.go index e2d80775b..a21513b7a 100644 --- a/cmd/mcs/server.go +++ b/cmd/mcs/server.go @@ -17,7 +17,7 @@ package main import ( - "flag" + "fmt" "log" "os" @@ -35,10 +35,35 @@ var serverCmd = cli.Command{ Usage: "starts mcs server", Action: startServer, Flags: []cli.Flag{ + cli.StringFlag{ + Name: "host", + Value: restapi.GetHostname(), + Usage: "HTTP server hostname", + }, cli.IntFlag{ Name: "port", - Value: 9090, - Usage: "Server port", + Value: restapi.GetPort(), + Usage: "HTTP Server port", + }, + cli.StringFlag{ + Name: "tls-host", + Value: restapi.GetSSLHostname(), + Usage: "HTTPS server hostname", + }, + cli.IntFlag{ + Name: "tls-port", + Value: restapi.GetSSLPort(), + Usage: "HTTPS server port", + }, + cli.StringFlag{ + Name: "tls-certificate", + Value: "", + Usage: "filename of public cert", + }, + cli.StringFlag{ + Name: "tls-key", + Value: "", + Usage: "filename of private key", }, }, } @@ -74,11 +99,31 @@ func startServer(ctx *cli.Context) error { } os.Exit(code) } - // Parse flags - flag.Parse() - server.ConfigureAPI() + + server.Host = ctx.String("host") server.Port = ctx.Int("port") + restapi.Hostname = ctx.String("host") + restapi.Port = fmt.Sprintf("%v",ctx.Int("port")) + + tlsCertificatePath := ctx.String("tls-certificate") + tlsCertificateKeyPath := ctx.String("tls-key") + + if tlsCertificatePath != "" && tlsCertificateKeyPath != "" { + server.TLSCertificate = flags.Filename(tlsCertificatePath) + server.TLSCertificateKey = flags.Filename(tlsCertificateKeyPath) + // If TLS certificates are provided enforce the HTTPS schema, meaning mcs will redirect + // plain HTTP connections to HTTPS server + server.EnabledListeners = []string{"http", "https"} + server.TLSPort = ctx.Int("tls-port") + server.TLSHost = ctx.String("tls-host") + // Need to store tls-port, tls-host un config variables so secure.middleware can read from there + restapi.TLSPort = fmt.Sprintf("%v",ctx.Int("tls-port")) + restapi.TLSHostname = ctx.String("tls-host") + } + + server.ConfigureAPI() + if err := server.Serve(); err != nil { log.Fatalln(err) } diff --git a/go.mod b/go.mod index fc16e9fb2..54ad8ccd2 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,11 @@ require ( github.com/go-openapi/validate v0.19.7 github.com/jessevdk/go-flags v1.4.0 github.com/minio/cli v1.22.0 + github.com/minio/m3/mcs v0.0.0-20200402043742-b25a986a7344 // indirect github.com/minio/mc v0.0.0-20200403024131-4d36c1f8b856 github.com/minio/minio v0.0.0-20200327214830-6f992134a25f github.com/minio/minio-go/v6 v6.0.51-0.20200401083717-eadbcae2a0e6 github.com/stretchr/testify v1.5.1 + github.com/unrolled/secure v1.0.7 golang.org/x/net v0.0.0-20200301022130-244492dfa37a ) diff --git a/go.sum b/go.sum index 8a9eda160..d4be0bc94 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,7 @@ github.com/cheggaaa/pb v1.0.28/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXH github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= github.com/coredns/coredns v1.4.0 h1:RubBkYmkByUqZWWkjRHvNLnUHgkRVqAWgSMmRFvpE1A= github.com/coredns/coredns v1.4.0/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= @@ -103,6 +104,7 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ER github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -383,8 +385,13 @@ github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2 github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs= github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA= +github.com/minio/m3 v0.0.2 h1:F2Oc0hPOLAAHYCjIcnSuKyeZVUbIO5ZSMzGV5Yeh3vU= +github.com/minio/m3/mcs v0.0.0-20200402043742-b25a986a7344 h1:IVYJFRNkDTsJlbGRmg6UN447DYCj3xnuHQOSoWWm1O4= +github.com/minio/m3/mcs v0.0.0-20200402043742-b25a986a7344/go.mod h1:uYD9TwIIxviKlQrrItSGxUSqGGQDm3hgkfrwr4sqeF4= +github.com/minio/mc v0.0.0-20200401220942-e05f02d9f459/go.mod h1:GWohdY5tXSiMnBCofmDRK5yRCihQH2FKNM0eh+UsY5Y= github.com/minio/mc v0.0.0-20200403024131-4d36c1f8b856 h1:4uIc5fw4tVr5glh2Mc8GFuiY04pTGEhmihPxJPUvCoU= github.com/minio/mc v0.0.0-20200403024131-4d36c1f8b856/go.mod h1:IDy4dA4aFY6zFFNkYgdUztl0jcYuev/Ubg3NadoaMKc= +github.com/minio/mcs v0.0.2 h1:3kdVL2oSa7u53cNRArDK4Ujiajjb56SK+Xb1/12Lu4Y= github.com/minio/minio v0.0.0-20200327214830-6f992134a25f h1:RoOBi0vhXkZqe2b6RTROOsVJUwMqLMoet9r7eL01euo= github.com/minio/minio v0.0.0-20200327214830-6f992134a25f/go.mod h1:BzbIyKUJPp+4f03i2XF7+GsijXnxMakUe5x+lm2WNc8= github.com/minio/minio-go/v6 v6.0.45/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg= @@ -549,6 +556,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/unrolled/secure v1.0.7 h1:BcQHp3iKZyZCKj5gRqwQG+5urnGBF00wGgoPPwtheVQ= +github.com/unrolled/secure v1.0.7/go.mod h1:uGc1OcRF8gCVBA+ANksKmvM85Hka6SZtQIbrKc3sHS4= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= diff --git a/restapi/client-admin.go b/restapi/client-admin.go index 567ae9428..9a1f75465 100644 --- a/restapi/client-admin.go +++ b/restapi/client-admin.go @@ -38,7 +38,7 @@ func NewAdminClient(url, accessKey, secretKey string) (*madmin.AdminClient, *pro AccessKey: accessKey, SecretKey: secretKey, AppName: appName, - AppVersion: Version, + AppVersion: McsVersion, AppComments: []string{appName, runtime.GOOS, runtime.GOARCH}, }) if err != nil { diff --git a/restapi/config.go b/restapi/config.go index 140cd2592..aa851c382 100644 --- a/restapi/config.go +++ b/restapi/config.go @@ -17,11 +17,18 @@ package restapi import ( + "fmt" + "strconv" "strings" "github.com/minio/minio/pkg/env" ) +var Port = "9090" +var TLSPort = "9443" +var Hostname = "localhost" +var TLSHostname = "localhost" + func getAccessKey() string { return env.Get(McsAccessKey, "minioadmin") } @@ -57,3 +64,142 @@ func getMinIOEndpointIsSecure() bool { } return false } + +func getProductionMode() bool { + return strings.ToLower(env.Get(McsProductionMode, "on")) == "on" +} + +func GetHostname() string { + return strings.ToLower(env.Get(McsHostname, Hostname)) +} + +func GetPort() int { + port, err := strconv.Atoi(env.Get(McsPort, Port)) + if err != nil { + port = 9090 + } + return port +} + +func GetSSLHostname() string { + return strings.ToLower(env.Get(McsTLSHostname, TLSHostname)) +} + +func GetSSLPort() int { + port, err := strconv.Atoi(env.Get(McsTLSPort, TLSPort)) + if err != nil { + port = 9443 + } + return port +} + +// Get secure middleware env variable configurations +func getSecureAllowedHosts() []string { + allowedHosts := env.Get(McsSecureAllowedHosts, "") + if allowedHosts != "" { + return strings.Split(allowedHosts, ",") + } else { + return []string{} + } +} + +// AllowedHostsAreRegex determines, if the provided AllowedHosts slice contains valid regular expressions. Default is false. +func getSecureAllowedHostsAreRegex() bool { + return strings.ToLower(env.Get(McsSecureAllowedHostsAreRegex, "off")) == "on" +} + +// If FrameDeny is set to true, adds the X-Frame-Options header with the value of `DENY`. Default is true. +func getSecureFrameDeny() bool { + return strings.ToLower(env.Get(McsSecureFrameDeny, "on")) == "on" +} + +// If ContentTypeNosniff is true, adds the X-Content-Type-Options header with the value `nosniff`. Default is true. +func getSecureContentTypeNonSniff() bool { + return strings.ToLower(env.Get(McsSecureContentTypeNoSniff, "on")) == "on" +} + +// If BrowserXssFilter is true, adds the X-XSS-Protection header with the value `1; mode=block`. Default is true. +func getSecureBrowserXssFilter() bool { + return strings.ToLower(env.Get(McsSecureBrowserXssFilter, "on")) == "on" +} + +// ContentSecurityPolicy 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. +func getSecureContentSecurityPolicy() string { + return env.Get(McsSecureContentSecurityPolicy, "") +} + +// ContentSecurityPolicyReportOnly allows the Content-Security-Policy-Report-Only header value to be set with a custom value. Default is "". +func getSecureContentSecurityPolicyReportOnly() string { + return env.Get(McsSecureContentSecurityPolicyReportOnly, "") +} + +// HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request. +func getSecureHostsProxyHeaders() []string { + allowedHosts := env.Get(McsSecureHostsProxyHeaders, "") + if allowedHosts != "" { + return strings.Split(allowedHosts, ",") + } else { + return []string{} + } +} + +// If SSLRedirect is set to true, then only allow HTTPS requests. Default is true. +func getSSLRedirect() bool { + return strings.ToLower(env.Get(McsSecureSSLRedirect, "on")) == "on" +} + +// SSLHost is the host name that is used to redirect HTTP requests to HTTPS. Default is "", which indicates to use the same host. +func getSecureSSLHost() string { + return env.Get(McsSecureSSLHost, fmt.Sprintf("%s:%s", TLSHostname, TLSPort)) +} + +// STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header. +func getSecureSTSSeconds() int64 { + seconds, err := strconv.Atoi(env.Get(McsSecureSTSSeconds, "0")) + if err != nil { + seconds = 0 + } + return int64(seconds) +} + +// If STSIncludeSubdomains is set to true, the `includeSubdomains` will be appended to the Strict-Transport-Security header. Default is false. +func getSecureSTSIncludeSubdomains() bool { + return strings.ToLower(env.Get(McsSecureSTSIncludeSubdomains, "off")) == "on" +} + +// If STSPreload is set to true, the `preload` flag will be appended to the Strict-Transport-Security header. Default is false. +func getSecureSTSPreload() bool { + return strings.ToLower(env.Get(McsSecureSTSPreload, "off")) == "on" +} + +// If SSLTemporaryRedirect is true, the a 302 will be used while redirecting. Default is false (301). +func getSecureSSLTemporaryRedirect() bool { + return strings.ToLower(env.Get(McsSecureSSLTemporaryRedirect, "off")) == "on" +} + +// STS header is only included when the connection is HTTPS. +func getSecureForceSTSHeader() bool { + return strings.ToLower(env.Get(McsSecureForceSTSHeader, "off")) == "on" +} + +// PublicKey implements HPKP to prevent MITM attacks with forged certificates. Default is "". +func getSecurePublicKey() string { + return env.Get(McsSecurePublicKey, "") +} + +// ReferrerPolicy allows the Referrer-Policy header with the value to be set with a custom value. Default is "". +func getSecureReferrerPolicy() string { + return env.Get(McsSecureReferrerPolicy, "") +} + +// FeaturePolicy allows the Feature-Policy header with the value to be set with a custom value. Default is "". +func getSecureFeaturePolicy() string { + return env.Get(McsSecureFeaturePolicy, "") +} + +// FeaturePolicy allows the Feature-Policy header with the value to be set with a custom value. Default is "". +func getSecureExpectCTHeader() string { + return env.Get(McsSecureExpectCTHeader, "") +} diff --git a/restapi/configure_mcs.go b/restapi/configure_mcs.go index 9a4a70e2d..8722b2763 100644 --- a/restapi/configure_mcs.go +++ b/restapi/configure_mcs.go @@ -35,6 +35,7 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime" "github.com/minio/mcs/restapi/operations" + "github.com/unrolled/secure" ) //go:generate swagger generate server --target ../../mcs --name Mcs --spec ../swagger.yml @@ -122,7 +123,34 @@ func setupMiddlewares(handler http.Handler) http.Handler { func setupGlobalMiddleware(handler http.Handler) http.Handler { // serve static files next := FileServerMiddleware(handler) - return next + // Secure middleware, this middleware wrap all the previous handlers and add + // HTTP security headers + secureOptions := secure.Options{ + AllowedHosts: getSecureAllowedHosts(), + AllowedHostsAreRegex: getSecureAllowedHostsAreRegex(), + HostsProxyHeaders: getSecureHostsProxyHeaders(), + SSLRedirect: getSSLRedirect(), + SSLHost: getSecureSSLHost(), + STSSeconds: getSecureSTSSeconds(), + STSIncludeSubdomains: getSecureSTSIncludeSubdomains(), + STSPreload: getSecureSTSPreload(), + SSLTemporaryRedirect: getSecureSSLTemporaryRedirect(), + SSLHostFunc: nil, + ForceSTSHeader: getSecureForceSTSHeader(), + FrameDeny: getSecureFrameDeny(), + ContentTypeNosniff: getSecureContentTypeNonSniff(), + BrowserXssFilter: getSecureBrowserXssFilter(), + ContentSecurityPolicy: getSecureContentSecurityPolicy(), + ContentSecurityPolicyReportOnly: getSecureContentSecurityPolicyReportOnly(), + PublicKey: getSecurePublicKey(), + ReferrerPolicy: getSecureReferrerPolicy(), + FeaturePolicy: getSecureFeaturePolicy(), + ExpectCTHeader: getSecureExpectCTHeader(), + IsDevelopment: !getProductionMode(), + } + secureMiddleware := secure.New(secureOptions) + app := secureMiddleware.Handler(next) + return app } // FileServerMiddleware serves files from the static folder diff --git a/restapi/consts.go b/restapi/consts.go index 1560881c9..a4518577d 100644 --- a/restapi/consts.go +++ b/restapi/consts.go @@ -17,8 +17,33 @@ package restapi const ( - Version = `0.1.0` - McsAccessKey = "MCS_ACCESS_KEY" - McsSecretKey = "MCS_SECRET_KEY" - McsMinIOServer = "MCS_MINIO_SERVER" + McsVersion = `0.1.0` + McsAccessKey = "MCS_ACCESS_KEY" + McsSecretKey = "MCS_SECRET_KEY" + McsMinIOServer = "MCS_MINIO_SERVER" + McsProductionMode = "MCS_PRODUCTION_MODE" + McsHostname = "MCS_HOSTNAME" + McsPort = "MCS_PORT" + McsTLSHostname = "MCS_TLS_HOSTNAME" + McsTLSPort = "MCS_TLS_PORT" + + McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS" + McsSecureAllowedHostsAreRegex = "MCS_SECURE_ALLOWED_HOSTS_ARE_REGEX" + McsSecureFrameDeny = "MCS_SECURE_FRAME_DENY" + McsSecureContentTypeNoSniff = "MCS_SECURE_CONTENT_TYPE_NO_SNIFF" + McsSecureBrowserXssFilter = "MCS_SECURE_BROWSER_XSS_FILTER" + McsSecureContentSecurityPolicy = "MCS_SECURE_CONTENT_SECURITY_POLICY" + McsSecureContentSecurityPolicyReportOnly = "MCS_SECURE_CONTENT_SECURITY_POLICY_REPORT_ONLY" + McsSecureHostsProxyHeaders = "MCS_SECURE_HOSTS_PROXY_HEADERS" + McsSecureSTSSeconds = "MCS_SECURE_STS_SECONDS" + McsSecureSTSIncludeSubdomains = "MCS_SECURE_STS_INCLUDE_SUB_DOMAINS" + McsSecureSTSPreload = "MCS_SECURE_STS_PRELOAD" + McsSecureSSLRedirect = "MCS_SECURE_SSL_REDIRECT" + McsSecureSSLHost = "MCS_SECURE_SSL_HOST" + McsSecureSSLTemporaryRedirect = "MCS_SECURE_SSL_TEMPORARY_REDIRECT" + McsSecureForceSTSHeader = "MCS_SECURE_FORCE_STS_HEADER" + McsSecurePublicKey = "MCS_SECURE_PUBLIC_KEY" + McsSecureReferrerPolicy = "MCS_SECURE_REFERRER_POLICY" + McsSecureFeaturePolicy = "MCS_SECURE_FEATURE_POLICY" + McsSecureExpectCTHeader = "MCS_SECURE_EXPECT_CT_HEADER" )