Skip to content

Commit

Permalink
Allow config of TLS cipher suites and min version (#256)
Browse files Browse the repository at this point in the history
* Server: allow config of TLS cipher suites and min version

Add a single parameter for each, not split across HTTP and gRPC.

Required change upstream - prometheus/exporter-toolkit#110.

Downstream projects rely on CLI parameters to generate docstrings,
so we add `--server.tls-cipher-suites` and `--server.tls-min-version`.
Both CLI and yaml require comma-separated lists of cipher suites,
which is different to the yaml array format supported by prometheus/exporter-toolkit.

The names accepted are from Go, listed here: https://pkg.go.dev/crypto/tls#pkg-constants

Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
  • Loading branch information
bboreham authored Sep 16, 2022
1 parent 3f07cc7 commit 9034106
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 8 deletions.
33 changes: 25 additions & 8 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ type Config struct {
GRPCListenPort int `yaml:"grpc_listen_port"`
GRPCConnLimit int `yaml:"grpc_listen_conn_limit"`

CipherSuites string `yaml:"tls_cipher_suites"`
MinVersion string `yaml:"tls_min_version"`
HTTPTLSConfig TLSConfig `yaml:"http_tls_config"`
GRPCTLSConfig TLSConfig `yaml:"grpc_tls_config"`

Expand Down Expand Up @@ -124,6 +126,8 @@ var infinty = time.Duration(math.MaxInt64)
func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
f.StringVar(&cfg.HTTPListenAddress, "server.http-listen-address", "", "HTTP server listen address.")
f.StringVar(&cfg.HTTPListenNetwork, "server.http-listen-network", DefaultNetwork, "HTTP server listen network, default tcp")
f.StringVar(&cfg.CipherSuites, "server.tls-cipher-suites", "", "Comma-separated list of cipher suites to use. If blank, the default Go cipher suites will be used.")
f.StringVar(&cfg.MinVersion, "server.tls-min-version", "", "Minimum TLS version to use. If blank, the Go TLS minimum version will be used.")
f.StringVar(&cfg.HTTPTLSConfig.TLSCertPath, "server.http-tls-cert-path", "", "HTTP server cert path.")
f.StringVar(&cfg.HTTPTLSConfig.TLSKeyPath, "server.http-tls-key-path", "", "HTTP server key path.")
f.StringVar(&cfg.HTTPTLSConfig.ClientAuth, "server.http-tls-client-auth", "", "HTTP TLS Client Auth type.")
Expand Down Expand Up @@ -243,15 +247,26 @@ func New(cfg Config) (*Server, error) {
grpcListener = netutil.LimitListener(grpcListener, cfg.GRPCConnLimit)
}

cipherSuites, err := stringToCipherSuites(cfg.CipherSuites)
if err != nil {
return nil, err
}
minVersion, err := stringToTLSVersion(cfg.MinVersion)
if err != nil {
return nil, err
}

// Setup TLS
var httpTLSConfig *tls.Config
if len(cfg.HTTPTLSConfig.TLSCertPath) > 0 && len(cfg.HTTPTLSConfig.TLSKeyPath) > 0 {
// Note: ConfigToTLSConfig from prometheus/exporter-toolkit is awaiting security review.
httpTLSConfig, err = web.ConfigToTLSConfig(&web.TLSStruct{
TLSCertPath: cfg.HTTPTLSConfig.TLSCertPath,
TLSKeyPath: cfg.HTTPTLSConfig.TLSKeyPath,
ClientAuth: cfg.HTTPTLSConfig.ClientAuth,
ClientCAs: cfg.HTTPTLSConfig.ClientCAs,
TLSCertPath: cfg.HTTPTLSConfig.TLSCertPath,
TLSKeyPath: cfg.HTTPTLSConfig.TLSKeyPath,
ClientAuth: cfg.HTTPTLSConfig.ClientAuth,
ClientCAs: cfg.HTTPTLSConfig.ClientCAs,
CipherSuites: cipherSuites,
MinVersion: minVersion,
})
if err != nil {
return nil, fmt.Errorf("error generating http tls config: %v", err)
Expand All @@ -261,10 +276,12 @@ func New(cfg Config) (*Server, error) {
if len(cfg.GRPCTLSConfig.TLSCertPath) > 0 && len(cfg.GRPCTLSConfig.TLSKeyPath) > 0 {
// Note: ConfigToTLSConfig from prometheus/exporter-toolkit is awaiting security review.
grpcTLSConfig, err = web.ConfigToTLSConfig(&web.TLSStruct{
TLSCertPath: cfg.GRPCTLSConfig.TLSCertPath,
TLSKeyPath: cfg.GRPCTLSConfig.TLSKeyPath,
ClientAuth: cfg.GRPCTLSConfig.ClientAuth,
ClientCAs: cfg.GRPCTLSConfig.ClientCAs,
TLSCertPath: cfg.GRPCTLSConfig.TLSCertPath,
TLSKeyPath: cfg.GRPCTLSConfig.TLSKeyPath,
ClientAuth: cfg.GRPCTLSConfig.ClientAuth,
ClientCAs: cfg.GRPCTLSConfig.ClientCAs,
CipherSuites: cipherSuites,
MinVersion: minVersion,
})
if err != nil {
return nil, fmt.Errorf("error generating grpc tls config: %v", err)
Expand Down
55 changes: 55 additions & 0 deletions server/tls_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package server

import (
"crypto/tls"
fmt "fmt"
"strings"

"github.com/prometheus/exporter-toolkit/web"
)

// Collect all cipher suite names and IDs recognized by Go, including insecure ones.
func allCiphers() map[string]web.Cipher {
acceptedCiphers := make(map[string]web.Cipher)
for _, suite := range tls.CipherSuites() {
acceptedCiphers[suite.Name] = web.Cipher(suite.ID)
}
for _, suite := range tls.InsecureCipherSuites() {
acceptedCiphers[suite.Name] = web.Cipher(suite.ID)
}
return acceptedCiphers
}

func stringToCipherSuites(s string) ([]web.Cipher, error) {
if s == "" {
return nil, nil
}
ciphersSlice := []web.Cipher{}
possibleCiphers := allCiphers()
for _, cipher := range strings.Split(s, ",") {
intValue, ok := possibleCiphers[cipher]
if !ok {
return nil, fmt.Errorf("cipher suite %q not recognized", cipher)
}
ciphersSlice = append(ciphersSlice, intValue)
}
return ciphersSlice, nil
}

// Using the same names that Kubernetes does
var tlsVersions = map[string]uint16{
"VersionTLS10": tls.VersionTLS10,
"VersionTLS11": tls.VersionTLS11,
"VersionTLS12": tls.VersionTLS12,
"VersionTLS13": tls.VersionTLS13,
}

func stringToTLSVersion(s string) (web.TLSVersion, error) {
if s == "" {
return 0, nil
}
if version, ok := tlsVersions[s]; ok {
return web.TLSVersion(version), nil
}
return 0, fmt.Errorf("TLS version %q not recognized", s)
}
59 changes: 59 additions & 0 deletions server/tls_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package server

import (
"crypto/tls"
"testing"

"github.com/prometheus/exporter-toolkit/web"
"github.com/stretchr/testify/require"
)

func Test_stringToCipherSuites(t *testing.T) {
tests := []struct {
name string
arg string
want []web.Cipher
wantErr bool
}{
{name: "blank", arg: "", want: nil},
{name: "bad", arg: "not-a-cipher", wantErr: true},
{name: "one", arg: "TLS_AES_256_GCM_SHA384", want: []web.Cipher{web.Cipher(tls.TLS_AES_256_GCM_SHA384)}},
{name: "two", arg: "TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256",
want: []web.Cipher{web.Cipher(tls.TLS_AES_256_GCM_SHA384), web.Cipher(tls.TLS_CHACHA20_POLY1305_SHA256)}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := stringToCipherSuites(tt.arg)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}

func Test_stringToTLSVersion(t *testing.T) {
tests := []struct {
name string
arg string
want web.TLSVersion
wantErr bool
}{
{name: "blank", arg: "", want: 0},
{name: "bad", arg: "not-a-version", wantErr: true},
{name: "VersionTLS12", arg: "VersionTLS12", want: tls.VersionTLS12},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := stringToTLSVersion(tt.arg)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}

0 comments on commit 9034106

Please sign in to comment.