summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Schalla <daniel@schalla.me>2018-10-16 16:51:46 +0200
committerChristopher Speller <crspeller@gmail.com>2018-10-16 07:51:46 -0700
commit557fd9ea187b1279b43ff63b94fedf2320aa3351 (patch)
tree463fdbd5aefba8f94a61fb1338bf5e7bd123a5f6
parentcedf6488e4d4d66c186facb4253513b1f7e775c6 (diff)
downloadchat-557fd9ea187b1279b43ff63b94fedf2320aa3351.tar.gz
chat-557fd9ea187b1279b43ff63b94fedf2320aa3351.tar.bz2
chat-557fd9ea187b1279b43ff63b94fedf2320aa3351.zip
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
-rw-r--r--app/server.go65
-rw-r--r--app/server_test.go147
-rw-r--r--config/default.json4
-rw-r--r--i18n/en.json12
-rw-r--r--model/config.go73
-rw-r--r--tests/tls_test_cert.pem20
-rw-r--r--tests/tls_test_key.pem28
-rw-r--r--web/handlers.go4
-rw-r--r--web/handlers_test.go51
9 files changed, 394 insertions, 10 deletions
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
@@ -4315,6 +4315,18 @@
"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")
+ }
+}