tls: enable the selection of more TLS settings (#1695)

tls: enable the selection of more TLS settings
* Rename `tls_config` to `tls_server_config`.
* Add new http server config with HTTP/2 enabled by default.

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
This commit is contained in:
Julien Pivotto 2020-05-13 20:26:01 +02:00 committed by GitHub
parent 0c532984b7
commit f87e566df9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 433 additions and 40 deletions

View file

@ -14,18 +14,48 @@ The config file should be written in YAML format, and is reloaded on each connec
## Sample Config
```
tls_config:
# Certificate and key files for server to use to authenticate to client
tls_server_config:
# Certificate and key files for server to use to authenticate to client.
cert_file: <filename>
key_file: <filename>
# Server policy for client authentication. Maps to ClientAuth Policies
# Server policy for client authentication. Maps to ClientAuth Policies.
# For more detail on clientAuth options: [ClientAuthType](https://golang.org/pkg/crypto/tls/#ClientAuthType)
[ client_auth_type: <string> | default = "NoClientCert" ]
# CA certificate for client certificate authentication to the server
# CA certificate for client certificate authentication to the server.
[ client_ca_file: <filename> ]
# Minimum TLS version that is acceptable.
[ min_version: <string> | default = "TLS12" ]
# Maximum TLS version that is acceptable.
[ max_version: <string> | default = "TLS13" ]
# List of supported cipher suites for TLS versions up to TLS 1.2. If empty,
# Go default cipher suites are used. Available cipher suites are documented
# in the go documentation:
# https://golang.org/pkg/crypto/tls/#pkg-constants
[ cipher_suites:
[ - <string> ] ]
# prefer_server_cipher_suites controls whether the server selects the
# client's most preferred ciphersuite, or the server's most preferred
# ciphersuite. If true then the server's preference, as expressed in
# the order of elements in cipher_suites, is used.
[ prefer_server_cipher_suites: <bool> | default = true ]
# Elliptic curves that will be used in an ECDHE handshake, in preference
# order. Available curves are documented in the go documentation:
# https://golang.org/pkg/crypto/tls/#CurveID
[ curve_preferences:
[ - <string> ] ]
http_server_config:
# Enable HTTP/2 support. Note that HTTP/2 is only supported with TLS.
# This can not be changed on the fly.
[ http2: <bool> | default = true ]
# List of usernames and hashed passwords that have full access to the web
# server via basic authentication. If empty, no basic authentication is
# required. Passwords are hashed with bcrypt.

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_ca_file : "somefile"

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "RequireAndVerifyClientCert"

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
basic_auth_users:

View file

@ -1,2 +1,2 @@
tls_config :
tls_server_config :
cert_filse: "testdata/server.crt"

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_ca_file : "testdata/tls-ca-chain.pem"

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "RequireAndVerifyClientCert"

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"

View file

@ -0,0 +1,26 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
cipher_suites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
- TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
- TLS_RSA_WITH_3DES_EDE_CBC_SHA
- TLS_RSA_WITH_AES_128_CBC_SHA
- TLS_RSA_WITH_AES_256_CBC_SHA
- TLS_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384

View file

@ -0,0 +1,10 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
curve_preferences:
- CurveP256
- CurveP384
- CurveP521
- X25519

View file

@ -1,3 +1,3 @@
tls_config :
tls_server_config :
cert_file : ""
key_file : "testdata/server.key"

View file

@ -1,3 +1,3 @@
tls_config :
tls_server_config :
cert_file : "somefile"
key_file : "testdata/server.key"

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : ""
key_file : ""
client_auth_type: "x"

View file

@ -1,3 +1,3 @@
tls_config :
tls_server_config :
cert_file : "somefile"
key_file : "somefile"

View file

@ -0,0 +1,8 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
cipher_suites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA2048

View file

@ -0,0 +1,7 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
curve_preferences:
- CurveP257

View file

@ -1,3 +1,3 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : ""

View file

@ -1,3 +1,3 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.cert"
key_file : "somefile"

View file

@ -0,0 +1,10 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
cipher_suites:
- TLS_RSA_WITH_AES_128_CBC_SHA
max_version: TLS12
http_server_config:
http2: false

View file

@ -0,0 +1,8 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
cipher_suites:
- TLS_RSA_WITH_AES_128_CBC_SHA
max_version: TLS12

View file

@ -0,0 +1,10 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
cipher_suites:
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
min_version: TLS12
max_version: TLS12

View file

@ -0,0 +1,11 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
cipher_suites:
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
prefer_server_cipher_suites: false
min_version: TLS12
max_version: TLS12

View file

@ -0,0 +1,8 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
min_version: TLS13
curve_preferences:
- CurveP521

View file

@ -0,0 +1,6 @@
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
client_auth_type : "VerifyClientCertIfGiven"
client_ca_file : "testdata/tls-ca-chain.pem"
min_version: TLS111

View file

@ -1,4 +1,4 @@
tls_config :
tls_server_config :
cert_file : "testdata/server.crt"
key_file : "testdata/server.key"
basic_auth_users:

View file

@ -17,6 +17,7 @@ package https
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
@ -32,15 +33,25 @@ var (
)
type Config struct {
TLSConfig TLSStruct `yaml:"tls_config"`
Users map[string]config_util.Secret `yaml:"basic_auth_users"`
TLSConfig TLSStruct `yaml:"tls_server_config"`
HTTPConfig HTTPStruct `yaml:"http_server_config"`
Users map[string]config_util.Secret `yaml:"basic_auth_users"`
}
type TLSStruct struct {
TLSCertPath string `yaml:"cert_file"`
TLSKeyPath string `yaml:"key_file"`
ClientAuth string `yaml:"client_auth_type"`
ClientCAs string `yaml:"client_ca_file"`
TLSCertPath string `yaml:"cert_file"`
TLSKeyPath string `yaml:"key_file"`
ClientAuth string `yaml:"client_auth_type"`
ClientCAs string `yaml:"client_ca_file"`
CipherSuites []cipher `yaml:"cipher_suites"`
CurvePreferences []curve `yaml:"curve_preferences"`
MinVersion tlsVersion `yaml:"min_version"`
MaxVersion tlsVersion `yaml:"max_version"`
PreferServerCipherSuites bool `yaml:"prefer_server_cipher_suites"`
}
type HTTPStruct struct {
HTTP2 bool `yaml:"http2"`
}
func getConfig(configPath string) (*Config, error) {
@ -48,7 +59,14 @@ func getConfig(configPath string) (*Config, error) {
if err != nil {
return nil, err
}
c := &Config{}
c := &Config{
TLSConfig: TLSStruct{
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
PreferServerCipherSuites: true,
},
HTTPConfig: HTTPStruct{HTTP2: true},
}
err = yaml.UnmarshalStrict(content, c)
return c, err
}
@ -70,12 +88,11 @@ func ConfigToTLSConfig(c *TLSStruct) (*tls.Config, error) {
if c.TLSCertPath == "" {
return nil, errors.New("missing cert_file")
}
if c.TLSKeyPath == "" {
return nil, errors.New("missing key_file")
}
cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
}
loadCert := func() (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(c.TLSCertPath, c.TLSKeyPath)
if err != nil {
@ -83,14 +100,38 @@ func ConfigToTLSConfig(c *TLSStruct) (*tls.Config, error) {
}
return &cert, nil
}
// Confirm that certificate and key paths are valid.
if _, err := loadCert(); err != nil {
return nil, err
}
cfg := &tls.Config{
MinVersion: (uint16)(c.MinVersion),
MaxVersion: (uint16)(c.MaxVersion),
PreferServerCipherSuites: c.PreferServerCipherSuites,
}
cfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return loadCert()
}
var cf []uint16
for _, c := range c.CipherSuites {
cf = append(cf, (uint16)(c))
}
if len(cf) > 0 {
cfg.CipherSuites = cf
}
var cp []tls.CurveID
for _, c := range c.CurvePreferences {
cp = append(cp, (tls.CurveID)(c))
}
if len(cp) > 0 {
cfg.CurvePreferences = cp
}
if c.ClientCAs != "" {
clientCAPool := x509.NewCertPool()
clientCAFile, err := ioutil.ReadFile(c.ClientCAs)
@ -126,7 +167,7 @@ func ConfigToTLSConfig(c *TLSStruct) (*tls.Config, error) {
// Listen starts the server on the given address. If tlsConfigPath isn't empty the server connection will be started using TLS.
func Listen(server *http.Server, tlsConfigPath string, logger log.Logger) error {
if tlsConfigPath == "" {
level.Info(logger).Log("msg", "TLS is disabled and it cannot be enabled on the fly.")
level.Info(logger).Log("msg", "TLS is disabled and it cannot be enabled on the fly.", "http2", false)
return server.ListenAndServe()
}
@ -145,14 +186,21 @@ func Listen(server *http.Server, tlsConfigPath string, logger log.Logger) error
handler: handler,
}
config, err := getTLSConfig(tlsConfigPath)
c, err := getConfig(tlsConfigPath)
if err != nil {
return err
}
config, err := ConfigToTLSConfig(&c.TLSConfig)
switch err {
case nil:
if !c.HTTPConfig.HTTP2 {
server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
}
// Valid TLS config.
level.Info(logger).Log("msg", "TLS is enabled and it cannot be disabled on the fly.")
level.Info(logger).Log("msg", "TLS is enabled and it cannot be disabled on the fly.", "http2", c.HTTPConfig.HTTP2)
case errNoTLSConfig:
// No TLS config, back to plain HTTP.
level.Info(logger).Log("msg", "TLS is disabled and it cannot be enabled on the fly.")
level.Info(logger).Log("msg", "TLS is disabled and it cannot be enabled on the fly.", "http2", false)
return server.ListenAndServe()
default:
// Invalid TLS config.
@ -168,3 +216,86 @@ func Listen(server *http.Server, tlsConfigPath string, logger log.Logger) error
}
return server.ListenAndServeTLS("", "")
}
type cipher uint16
func (c *cipher) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
err := unmarshal((*string)(&s))
if err != nil {
return err
}
for _, cs := range tls.CipherSuites() {
if cs.Name == s {
*c = (cipher)(cs.ID)
return nil
}
}
return errors.New("unknown cipher: " + s)
}
func (c cipher) MarshalYAML() (interface{}, error) {
return tls.CipherSuiteName((uint16)(c)), nil
}
type curve tls.CurveID
var curves = map[string]curve{
"CurveP256": (curve)(tls.CurveP256),
"CurveP384": (curve)(tls.CurveP384),
"CurveP521": (curve)(tls.CurveP521),
"X25519": (curve)(tls.X25519),
}
func (c *curve) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
err := unmarshal((*string)(&s))
if err != nil {
return err
}
if curveid, ok := curves[s]; ok {
*c = curveid
return nil
}
return errors.New("unknown curve: " + s)
}
func (c *curve) MarshalYAML() (interface{}, error) {
for s, curveid := range curves {
if *c == curveid {
return s, nil
}
}
return fmt.Sprintf("%v", c), nil
}
type tlsVersion uint16
var tlsVersions = map[string]tlsVersion{
"TLS13": (tlsVersion)(tls.VersionTLS13),
"TLS12": (tlsVersion)(tls.VersionTLS12),
"TLS11": (tlsVersion)(tls.VersionTLS11),
"TLS10": (tlsVersion)(tls.VersionTLS10),
}
func (tv *tlsVersion) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
err := unmarshal((*string)(&s))
if err != nil {
return err
}
if v, ok := tlsVersions[s]; ok {
*tv = v
return nil
}
return errors.New("unknown TLS version: " + s)
}
func (tv *tlsVersion) MarshalYAML() (interface{}, error) {
for s, v := range tlsVersions {
if *tv == v {
return s, nil
}
}
return fmt.Sprintf("%v", tv), nil
}

View file

@ -11,6 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// +build go1.14
package https
import (
@ -45,6 +47,12 @@ var (
"Bad password": regexp.MustCompile(`hashedSecret too short to be a bcrypted password`),
"Unauthorized": regexp.MustCompile(`Unauthorized`),
"Forbidden": regexp.MustCompile(`Forbidden`),
"Handshake failure": regexp.MustCompile(`handshake failure`),
"Unknown cipher": regexp.MustCompile(`unknown cipher`),
"Unknown curve": regexp.MustCompile(`unknown curve`),
"Unknown TLS version": regexp.MustCompile(`unknown TLS version`),
"No HTTP2 cipher": regexp.MustCompile(`TLSConfig.CipherSuites is missing an HTTP/2-required`),
"Incompatible TLS version": regexp.MustCompile(`protocol version not supported`),
}
)
@ -65,14 +73,18 @@ func getPort() string {
}
type TestInputs struct {
Name string
Server func() *http.Server
UseNilServer bool
YAMLConfigPath string
ExpectedError *regexp.Regexp
UseTLSClient bool
Username string
Password string
Name string
Server func() *http.Server
UseNilServer bool
YAMLConfigPath string
ExpectedError *regexp.Regexp
UseTLSClient bool
ClientMaxTLSVersion uint16
CipherSuites []uint16
ActualCipher uint16
CurvePreferences []tls.CurveID
Username string
Password string
}
func TestYAMLFiles(t *testing.T) {
@ -142,6 +154,21 @@ func TestYAMLFiles(t *testing.T) {
YAMLConfigPath: "testdata/tls_config_auth_user_list_invalid.bad.yml",
ExpectedError: ErrorMap["Bad password"],
},
{
Name: `invalid config yml (bad cipher)`,
YAMLConfigPath: "testdata/tls_config_noAuth_inventedCiphers.bad.yml",
ExpectedError: ErrorMap["Unknown cipher"],
},
{
Name: `invalid config yml (bad curves)`,
YAMLConfigPath: "testdata/tls_config_noAuth_inventedCurves.bad.yml",
ExpectedError: ErrorMap["Unknown curve"],
},
{
Name: `invalid config yml (bad TLS version)`,
YAMLConfigPath: "testdata/tls_config_noAuth_wrongTLSVersion.bad.yml",
ExpectedError: ErrorMap["Unknown TLS version"],
},
}
for _, testInputs := range testTables {
t.Run(testInputs.Name, testInputs.Test)
@ -172,6 +199,87 @@ func TestServerBehaviour(t *testing.T) {
UseTLSClient: true,
ExpectedError: nil,
},
{
Name: `valid tls config yml with TLS 1.1 client`,
YAMLConfigPath: "testdata/tls_config_noAuth.good.yml",
UseTLSClient: true,
ClientMaxTLSVersion: tls.VersionTLS11,
ExpectedError: ErrorMap["Incompatible TLS version"],
},
{
Name: `valid tls config yml with all ciphers`,
YAMLConfigPath: "testdata/tls_config_noAuth_allCiphers.good.yml",
UseTLSClient: true,
ExpectedError: nil,
},
{
Name: `valid tls config yml with some ciphers`,
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers.good.yml",
UseTLSClient: true,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
ExpectedError: nil,
},
{
Name: `valid tls config yml with no common cipher`,
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers.good.yml",
UseTLSClient: true,
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA},
ExpectedError: ErrorMap["Handshake failure"],
},
{
Name: `valid tls config yml with multiple client ciphers`,
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers.good.yml",
UseTLSClient: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},
ActualCipher: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
ExpectedError: nil,
},
{
Name: `valid tls config yml with multiple client ciphers, client chooses cipher`,
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers_noOrder.good.yml",
UseTLSClient: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},
ActualCipher: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
ExpectedError: nil,
},
{
Name: `valid tls config yml with all curves`,
YAMLConfigPath: "testdata/tls_config_noAuth_allCurves.good.yml",
UseTLSClient: true,
ExpectedError: nil,
},
{
Name: `valid tls config yml with some curves`,
YAMLConfigPath: "testdata/tls_config_noAuth_someCurves.good.yml",
UseTLSClient: true,
CurvePreferences: []tls.CurveID{tls.CurveP521},
ExpectedError: nil,
},
{
Name: `valid tls config yml with no common curves`,
YAMLConfigPath: "testdata/tls_config_noAuth_someCurves.good.yml",
UseTLSClient: true,
CurvePreferences: []tls.CurveID{tls.CurveP384},
ExpectedError: ErrorMap["Handshake failure"],
},
{
Name: `valid tls config yml with non-http2 ciphers`,
YAMLConfigPath: "testdata/tls_config_noAuth_noHTTP2.good.yml",
UseTLSClient: true,
ExpectedError: nil,
},
{
Name: `valid tls config yml with non-http2 ciphers but http2 enabled`,
YAMLConfigPath: "testdata/tls_config_noAuth_noHTTP2Cipher.bad.yml",
UseTLSClient: true,
ExpectedError: ErrorMap["No HTTP2 cipher"],
},
}
for _, testInputs := range testTables {
t.Run(testInputs.Name, testInputs.Test)
@ -297,6 +405,14 @@ func (test *TestInputs) Test(t *testing.T) {
var proto string
if test.UseTLSClient {
client = getTLSClient()
t := client.Transport.(*http.Transport)
t.TLSClientConfig.MaxVersion = test.ClientMaxTLSVersion
if len(test.CipherSuites) > 0 {
t.TLSClientConfig.CipherSuites = test.CipherSuites
}
if len(test.CurvePreferences) > 0 {
t.TLSClientConfig.CurvePreferences = test.CurvePreferences
}
proto = "https"
} else {
client = http.DefaultClient
@ -318,6 +434,18 @@ func (test *TestInputs) Test(t *testing.T) {
recordConnectionError(err)
return
}
if test.ActualCipher != 0 {
if r.TLS.CipherSuite != test.ActualCipher {
recordConnectionError(
fmt.Errorf("bad cipher suite selected. Expected: %s, got: %s",
tls.CipherSuiteName(r.TLS.CipherSuite),
tls.CipherSuiteName(test.ActualCipher),
),
)
}
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
recordConnectionError(err)