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" )