From 557fd9ea187b1279b43ff63b94fedf2320aa3351 Mon Sep 17 00:00:00 2001 From: Daniel Schalla Date: Tue, 16 Oct 2018 16:51:46 +0200 Subject: Set default ciphers, set tls 1.2 via config, set curve prefs (#9315) Config Checks at StartUp Part1 Config Checks; Tests for TLS Server HSTS header implementation + tests make gofmt happy with new go version... make gofmt happy with new go version #2... fix logic bug fix typo Fix unnecessary code block --- app/server.go | 65 ++++++++++++++++++--- app/server_test.go | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ config/default.json | 4 ++ i18n/en.json | 12 ++++ model/config.go | 73 ++++++++++++++++++++++++ tests/tls_test_cert.pem | 20 +++++++ tests/tls_test_key.pem | 28 +++++++++ web/handlers.go | 4 ++ web/handlers_test.go | 51 ++++++++++++++++- 9 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 tests/tls_test_cert.pem create mode 100644 tests/tls_test_key.pem diff --git a/app/server.go b/app/server.go index debb6764f..b95059c84 100644 --- a/app/server.go +++ b/app/server.go @@ -46,7 +46,7 @@ type Server struct { didFinishListen chan struct{} } -var corsAllowedMethods []string = []string{ +var corsAllowedMethods = []string{ "POST", "GET", "OPTIONS", @@ -199,26 +199,75 @@ func (a *App) StartServer() error { go func() { var err error if *a.Config().ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { - if *a.Config().ServiceSettings.UseLetsEncrypt { - tlsConfig := &tls.Config{ - GetCertificate: m.GetCertificate, + tlsConfig := &tls.Config{ + PreferServerCipherSuites: true, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + } + + switch *a.Config().ServiceSettings.TLSMinVer { + case "1.0": + tlsConfig.MinVersion = tls.VersionTLS10 + case "1.1": + tlsConfig.MinVersion = tls.VersionTLS11 + default: + tlsConfig.MinVersion = tls.VersionTLS12 + } + + defaultCiphers := []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + } + + if len(a.Config().ServiceSettings.TLSOverwriteCiphers) == 0 { + tlsConfig.CipherSuites = defaultCiphers + } else { + var cipherSuites []uint16 + for _, cipher := range a.Config().ServiceSettings.TLSOverwriteCiphers { + value, ok := model.ServerTLSSupportedCiphers[cipher] + + if !ok { + mlog.Warn("Unsupported cipher passed", mlog.String("cipher", cipher)) + continue + } + + cipherSuites = append(cipherSuites, value) } - tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + if len(cipherSuites) == 0 { + mlog.Warn("No supported ciphers passed, fallback to default cipher suite") + cipherSuites = defaultCiphers + } + + tlsConfig.CipherSuites = cipherSuites + } + + certFile := "" + keyFile := "" - a.Srv.Server.TLSConfig = tlsConfig - err = a.Srv.Server.ServeTLS(listener, "", "") + if *a.Config().ServiceSettings.UseLetsEncrypt { + tlsConfig.GetCertificate = m.GetCertificate + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") } else { - err = a.Srv.Server.ServeTLS(listener, *a.Config().ServiceSettings.TLSCertFile, *a.Config().ServiceSettings.TLSKeyFile) + certFile = *a.Config().ServiceSettings.TLSCertFile + keyFile = *a.Config().ServiceSettings.TLSKeyFile } + + a.Srv.Server.TLSConfig = tlsConfig + err = a.Srv.Server.ServeTLS(listener, certFile, keyFile) } else { err = a.Srv.Server.Serve(listener) } + if err != nil && err != http.ErrServerClosed { mlog.Critical(fmt.Sprintf("Error starting server, err:%v", err)) time.Sleep(time.Second) } + close(a.Srv.didFinishListen) }() diff --git a/app/server_test.go b/app/server_test.go index 94771a44e..4a355e113 100644 --- a/app/server_test.go +++ b/app/server_test.go @@ -4,6 +4,12 @@ package app import ( + "crypto/tls" + "github.com/mattermost/mattermost-server/utils" + "net/http" + "path" + "strconv" + "strings" "testing" "github.com/mattermost/mattermost-server/model" @@ -16,6 +22,10 @@ func TestStartServerSuccess(t *testing.T) { a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) serverErr := a.StartServer() + + client := &http.Client{} + checkEndpoint(t, client, "http://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + a.Shutdown() require.NoError(t, serverErr) } @@ -48,3 +58,140 @@ func TestStartServerPortUnavailable(t *testing.T) { a.Shutdown() require.Error(t, serverErr) } + +func TestStartServerTLSSuccess(t *testing.T) { + a, err := New() + require.NoError(t, err) + + testDir, _ := utils.FindDir("tests") + a.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ConnectionSecurity = "TLS" + *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") + *cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem") + }) + serverErr := a.StartServer() + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + + a.Shutdown() + require.NoError(t, serverErr) +} + +func TestStartServerTLSVersion(t *testing.T) { + a, err := New() + require.NoError(t, err) + + testDir, _ := utils.FindDir("tests") + a.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ConnectionSecurity = "TLS" + *cfg.ServiceSettings.TLSMinVer = "1.2" + *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") + *cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem") + }) + serverErr := a.StartServer() + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + MaxVersion: tls.VersionTLS11, + }, + } + + client := &http.Client{Transport: tr} + err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + + if !strings.Contains(err.Error(), "remote error: tls: protocol version not supported") { + t.Errorf("Expected protocol version error, got %s", err) + } + + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + + if err != nil { + t.Errorf("Expected nil, got %s", err) + } + + a.Shutdown() + require.NoError(t, serverErr) +} + +func TestStartServerTLSOverwriteCipher(t *testing.T) { + a, err := New() + require.NoError(t, err) + + testDir, _ := utils.FindDir("tests") + a.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ConnectionSecurity = "TLS" + cfg.ServiceSettings.TLSOverwriteCiphers = []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + } + *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") + *cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem") + }) + serverErr := a.StartServer() + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + }, + }, + } + + client := &http.Client{Transport: tr} + err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + + if !strings.Contains(err.Error(), "remote error: tls: handshake failure") { + t.Errorf("Expected protocol version error, got %s", err) + } + + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + }, + } + + err = checkEndpoint(t, client, "https://localhost:" + strconv.Itoa(a.Srv.ListenAddr.Port) + "/", http.StatusNotFound) + + if err != nil { + t.Errorf("Expected nil, got %s", err) + } + + a.Shutdown() + require.NoError(t, serverErr) +} + +func checkEndpoint(t *testing.T, client *http.Client, url string, expectedStatus int) error { + res, err := client.Get(url) + + if err != nil { + return err + } + + defer res.Body.Close() + + if res.StatusCode != expectedStatus { + t.Errorf("Response code was %d; want %d", res.StatusCode, expectedStatus) + } + + return nil +} diff --git a/config/default.json b/config/default.json index b303365b5..14f8248ff 100644 --- a/config/default.json +++ b/config/default.json @@ -7,6 +7,10 @@ "ConnectionSecurity": "", "TLSCertFile": "", "TLSKeyFile": "", + "TLSMinVer": "1.2", + "TLSStrictTransport": false, + "TLSStrictTransportMaxAge": 63072000, + "TLSOverwriteCiphers": [], "UseLetsEncrypt": false, "LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache", "Forward80To443": false, diff --git a/i18n/en.json b/i18n/en.json index db325eee0..d5a6f519a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4314,6 +4314,18 @@ "id": "model.config.is_valid.webserver_security.app_error", "translation": "Invalid value for webserver connection security." }, + { + "id": "model.config.is_valid.tls_cert_file.app_error", + "translation": "Invalid value for TLS certificate file - Either use LetsEncrypt or set path to existing certificate file" + }, + { + "id": "model.config.is_valid.tls_key_file.app_error", + "translation": "Invalid value for TLS key file - Either use LetsEncrypt or set path to existing key file" + }, + { + "id": "model.config.is_valid.tls_overwrite_cipher.app_error", + "translation": "Invalid value passed for TLS overwrite cipher - Please refer to the documentation for valid values" + }, { "id": "model.config.is_valid.websocket_url.app_error", "translation": "Websocket URL must be a valid URL and start with ws:// or wss://" diff --git a/model/config.go b/model/config.go index d59b8d6db..dcf3adff5 100644 --- a/model/config.go +++ b/model/config.go @@ -4,12 +4,14 @@ package model import ( + "crypto/tls" "encoding/json" "io" "math" "net" "net/http" "net/url" + "os" "regexp" "strconv" "strings" @@ -174,6 +176,31 @@ const ( CLIENT_SIDE_CERT_CHECK_SECONDARY_AUTH = "secondary" ) +var ServerTLSSupportedCiphers = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, +} + type ServiceSettings struct { SiteURL *string WebsocketURL *string @@ -182,6 +209,10 @@ type ServiceSettings struct { ConnectionSecurity *string TLSCertFile *string TLSKeyFile *string + TLSMinVer *string + TLSStrictTransport *bool + TLSStrictTransportMaxAge *int64 + TLSOverwriteCiphers []string UseLetsEncrypt *bool LetsEncryptCertificateCacheFile *string Forward80To443 *bool @@ -324,6 +355,22 @@ func (s *ServiceSettings) SetDefaults() { s.TLSCertFile = NewString(SERVICE_SETTINGS_DEFAULT_TLS_CERT_FILE) } + if s.TLSMinVer == nil { + s.TLSMinVer = NewString("1.2") + } + + if s.TLSStrictTransport == nil { + s.TLSStrictTransport = NewBool(false) + } + + if s.TLSStrictTransportMaxAge == nil { + s.TLSStrictTransportMaxAge = NewInt64(63072000) + } + + if s.TLSOverwriteCiphers == nil { + s.TLSOverwriteCiphers = []string{} + } + if s.UseLetsEncrypt == nil { s.UseLetsEncrypt = NewBool(false) } @@ -2352,6 +2399,32 @@ func (ss *ServiceSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.webserver_security.app_error", nil, "", http.StatusBadRequest) } + if *ss.ConnectionSecurity == CONN_SECURITY_TLS && *ss.UseLetsEncrypt == false { + appErr := NewAppError("Config.IsValid", "model.config.is_valid.tls_cert_file.app_error", nil, "", http.StatusBadRequest) + + if *ss.TLSCertFile == "" { + return appErr + } else if _, err := os.Stat(*ss.TLSCertFile); os.IsNotExist(err) { + return appErr + } + + appErr = NewAppError("Config.IsValid", "model.config.is_valid.tls_key_file.app_error", nil, "", http.StatusBadRequest) + + if *ss.TLSKeyFile == "" { + return appErr + } else if _, err := os.Stat(*ss.TLSKeyFile); os.IsNotExist(err) { + return appErr + } + } + + if len(ss.TLSOverwriteCiphers) > 0 { + for _, cipher := range ss.TLSOverwriteCiphers { + if _, ok := ServerTLSSupportedCiphers[cipher]; !ok { + return NewAppError("Config.IsValid", "model.config.is_valid.tls_overwrite_cipher.app_error", map[string]interface{}{"name": cipher}, "", http.StatusBadRequest) + } + } + } + if *ss.ReadTimeout <= 0 { return NewAppError("Config.IsValid", "model.config.is_valid.read_timeout.app_error", nil, "", http.StatusBadRequest) } diff --git a/tests/tls_test_cert.pem b/tests/tls_test_cert.pem new file mode 100644 index 000000000..9b302132a --- /dev/null +++ b/tests/tls_test_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIJAK0E8sSdFSXnMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg +Q29tcGFueSBMdGQwHhcNMTgxMDA2MjAwMzQ1WhcNMTkxMDA2MjAwMzQ1WjBCMQsw +CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh +dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +0VVolTOXKn3sVwKivDJH015wa4f3c+9OrCVwdg7ZTaiebDutLT2kHi/H8cElpi67 +cE8ios4hPYfF+jJpNF1VMuLUtq93UWwUYhb709AvArMSA5L5Vz+0z/VPysu03jrC +o21A4U2G+LXjgOp8oSeRnfHRz6LrQxl1GTl2Kavou41SIpaf7YTM9za51Ya5wnUI +6UoCEnsgI2qVgeHdF9dMpV5XY5VM0lROdoZqtpRjJxe5wFZkW6JOpWpirX1h6jPq +KsY3CacCwonFrGM11mhcliBEGnUGEqNwBUrTkt2opTJ/L3KLFNC65ukg1y4RfFHz +1uQ/IrpNjpClT4AnlwkAmQIDAQABo1MwUTAdBgNVHQ4EFgQUxI+gQOaOrxtW+wHG +vFCqeipS1s8wHwYDVR0jBBgwFoAUxI+gQOaOrxtW+wHGvFCqeipS1s8wDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAKGXu7u23BMKw0sPjLB9Nu3vp +0Ova9mflJ4mdI5Y1mClwdN6ysIibC3RDQZbcsz4oEFOh11+z+xLmADhBzEGksuDs +9ZV2EAOvlFZpsl/2z1ACDq9NF6ru1slspOp2JRr74LIDo2iU4a1ubW6l1NN7bM3d +2AWjKmeaqwyT80sGVO0ZCYO9QsIgX+WR4SyaY1xUATJEs8gW64jBzcJQSnZZbHjt +eArVVzwUCAOp1XJhoXP/l9zOg/vtw8PUwb8iPZGK/8UtYTuHgVQUsYr6NAlds4Ag +IQMsphOLsvz8tXEsQAvd3e0Sz94/oZP1aDUopom9gPDNGOBykuWW0epBfv950A== +-----END CERTIFICATE----- diff --git a/tests/tls_test_key.pem b/tests/tls_test_key.pem new file mode 100644 index 000000000..41d33216f --- /dev/null +++ b/tests/tls_test_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRVWiVM5cqfexX +AqK8MkfTXnBrh/dz706sJXB2DtlNqJ5sO60tPaQeL8fxwSWmLrtwTyKiziE9h8X6 +Mmk0XVUy4tS2r3dRbBRiFvvT0C8CsxIDkvlXP7TP9U/Ky7TeOsKjbUDhTYb4teOA +6nyhJ5Gd8dHPoutDGXUZOXYpq+i7jVIilp/thMz3NrnVhrnCdQjpSgISeyAjapWB +4d0X10ylXldjlUzSVE52hmq2lGMnF7nAVmRbok6lamKtfWHqM+oqxjcJpwLCicWs +YzXWaFyWIEQadQYSo3AFStOS3ailMn8vcosU0Lrm6SDXLhF8UfPW5D8iuk2OkKVP +gCeXCQCZAgMBAAECggEBALRkQYexuabodO5WWx6KxdKkI4TG2ruRkd5PNSbHjQOb +N0pV8tp1sCRDUK5In8UhqG0UBOj/cS2w/y6omniBpZYAWwZDFzOXS8lrvP+++4P8 +BJ4H3c8OGybKY0SDXw3S3UAwOiTtxk41kCPb7iKCEr5lUUT5RHvCSGLAXc9zUU+s +3d4vscDTc/aLzm4e/hlnzA6pPQuz/GustgBDqonQRKTnKZ1pnzl7cBw0rOIJtmHe +pViS8g2jtGMB5NoIFj4u08MwKuKJDmr+vQxjMkyx05x+aN4QIR/6b4kw46wridAJ +zKIrMwWRXcFQWLs08LuTKmqwdmuDfdzHRJtnTCMvf7ECgYEA7n21yG6EOmeZWRJH +5fSwwttXbxx0JrfWhoBam8/RSumnFx1VUj+0vafO6W+USa5HGWJJ5FFRnpA6/OmD +Ywv9eHKUY3HtGXiZ536Pt/dPfBLX/FFzMlscCUAT6UvYOqytYngoKQqQeSeHDjPJ +HC8QyAW/gKOLvd2wdBHNZOT7Vp8CgYEA4LOz7hqVgHmIEaVSY+4WNCW/m4PPx/oW +as1D+V1eqLzzHDVDmivOGiyP+SIN9+BARCdTi8TPDkzkqOOYAt1kU5Neo3DoH9i3 +0qteQjy1xLA9UqAwOr16YpoPM/pBb6iwNTrj88Kcfs163hhMoDnarq6y1h9UYKgF +vJH5cIeudccCgYBRdptLdYSxNoYJCNeKUwS16pp5F60NNKqQkvNgWaJSBnHO0XQ9 +fglM5y8kSbrLWD5tC0fWN3i7wuSDU3hPst7H78uEFHw6wRlBG9gXrOB3rzAbve6t +erWe60Zh4Ehh8m3fPs/pBPTIjZnyXfoKKIGA8YWyeSrYlgsZ+qLAHf9EXQKBgQDb +WnI26VqyrXFIkJQKm3yvcX5IKXfoJ1pE7pcB0sU6giHtko2o7kRnxsLRmQ37wb3b +Cm0Dj5/1vNinim51tXxgHggQE4N2u1BP5xzAGpXzKXzjsR8D6L6VjQF0Y0QH5awG +erPW3U96dcsRDrWW4IN7bW2Fm9X5+WyINhREZx/HNwKBgH/RMcGhiIcpI4IPGmNi +udFhUd60izh5Gimyg8yBSPmX1xI6zjwPXiUvHpPWz65OZk1eltZgoDYvSe/rauJv +8VtdisTqZwUQG5mogQxo6uofjzKqQQLiJtmOrL1ZzJU8vJNKdiJTIs22wrSxTenj +QtIQkWnBO5bOVY/99s5wlzKw +-----END PRIVATE KEY----- diff --git a/web/handlers.go b/web/handlers.go index 71a43bc48..9b0705a5b 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -75,6 +75,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil)) + if *c.App.Config().ServiceSettings.TLSStrictTransport { + w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge)) + } + if h.IsStatic { // Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking w.Header().Set("X-Frame-Options", "SAMEORIGIN") diff --git a/web/handlers_test.go b/web/handlers_test.go index 0b9073fff..6b68a9987 100644 --- a/web/handlers_test.go +++ b/web/handlers_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -func handlerForTest(c *Context, w http.ResponseWriter, r *http.Request) { +func handlerForHTTPErrors(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = model.NewAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound) } @@ -25,7 +25,7 @@ func TestHandlerServeHTTPErrors(t *testing.T) { if err != nil { panic(err) } - handler := web.NewHandler(handlerForTest) + handler := web.NewHandler(handlerForHTTPErrors) var flagtests = []struct { name string @@ -57,3 +57,50 @@ func TestHandlerServeHTTPErrors(t *testing.T) { }) } } + +func handlerForHTTPSecureTransport(c *Context, w http.ResponseWriter, r *http.Request) { +} + +func TestHandlerServeHTTPSecureTransport(t *testing.T) { + a, err := app.New(app.StoreOverride(testStore), app.DisableConfigWatch) + defer a.Shutdown() + + a.UpdateConfig(func(config *model.Config) { + *config.ServiceSettings.TLSStrictTransport = true + *config.ServiceSettings.TLSStrictTransportMaxAge = 6000 + }) + + web := NewWeb(a, a.Srv.Router) + if err != nil { + panic(err) + } + handler := web.NewHandler(handlerForHTTPSecureTransport) + + request := httptest.NewRequest("GET", "/api/v4/test", nil) + + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + header := response.Header().Get("Strict-Transport-Security") + + if header == "" { + t.Errorf("Strict-Transport-Security expected but not existent") + } + + if header != "max-age=6000" { + t.Errorf("Expected max-age=6000, got %s", header) + } + + a.UpdateConfig(func(config *model.Config) { + *config.ServiceSettings.TLSStrictTransport = false + }) + + request = httptest.NewRequest("GET", "/api/v4/test", nil) + + response = httptest.NewRecorder() + handler.ServeHTTP(response, request) + header = response.Header().Get("Strict-Transport-Security") + + if header != "" { + t.Errorf("Strict-Transport-Security header is not expected, but returned") + } +} -- cgit v1.2.3-1-g7c22