Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio

- **General**: Add CRD-level validation markers (Minimum, MinLength, MinItems, Enum) for ScaledObject, ScaledJob, ScaleTriggers, and TriggerAuthentication API types ([#7533](https://github.com/kedacore/keda/pull/7533))
- **General**: Add `--leader-election-id` flag to allow configuring the leader election Lease name ([#7564](https://github.com/kedacore/keda/issues/7564))
- **General**: Allow more control of TLS versions & ciphers via `KEDA_HTTP_TLS_CIPHER_LIST`, `KEDA_SERVICE_TLS_CIPHER_LIST` and `KEDA_SERVICE_MIN_TLS_VERSION` env vars ([#7617](https://github.com/kedacore/keda/pull/7617))
- **General**: Make APIService cert injections optional ([#7559](https://github.com/kedacore/keda/pull/7559))
- **Elasticsearch Scaler**: Add HTTP status check for Elasticsearch errors ([#7480](https://github.com/kedacore/keda/pull/7480))
- **Kubernetes Workload Scaler**: Add `groupByNode` parameter ([#7628](https://github.com/kedacore/keda/issues/7628))
Expand Down
3 changes: 2 additions & 1 deletion cmd/webhooks/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ func main() {
CertDir: certDir,
TLSOpts: []func(tlsConfig *tls.Config){
func(tlsConfig *tls.Config) {
tlsConfig.MinVersion = kedautil.GetMinTLSVersion()
tlsConfig.MinVersion = kedautil.GetServiceMinTLSVersion()
tlsConfig.CipherSuites = kedautil.GetServiceTLSCipherList()
},
},
}),
Expand Down
5 changes: 4 additions & 1 deletion pkg/metricsservice/utils/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"github.com/fsnotify/fsnotify"
"google.golang.org/grpc/credentials"
logf "sigs.k8s.io/controller-runtime/pkg/log"

kedautil "github.com/kedacore/keda/v2/pkg/util"
)

var log = logf.Log.WithName("grpc_server_certificates")
Expand Down Expand Up @@ -127,7 +129,8 @@ func LoadGrpcTLSCredentials(ctx context.Context, certDir string, server bool) (c

// Create the credentials and return it
config := &tls.Config{
MinVersion: tls.VersionTLS13,
MinVersion: kedautil.GetServiceMinTLSVersion(),
CipherSuites: kedautil.GetServiceTLSCipherList(),
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
certMutex.RLock()
defer certMutex.RUnlock()
Expand Down
80 changes: 69 additions & 11 deletions pkg/util/tls_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,37 @@ import (
"encoding/pem"
"fmt"
"os"
"strings"

"github.com/youmark/pkcs8"
ctrl "sigs.k8s.io/controller-runtime"
)

var minTLSVersion uint16
var tlsCipherList []uint16
var serviceMinTLSVersion uint16
var serviceTLSCipherList []uint16

func init() {
var err error

if minTLSVersion, err = initMinTLSVersion(); err != nil {
ctrl.Log.WithName("tls_setup").Info(err.Error())
if minTLSVersion, err = ParseTLSVersion(os.Getenv("KEDA_HTTP_MIN_TLS_VERSION"), tls.VersionTLS12); err != nil {
ctrl.Log.WithName("tls_setup").Info("Error parsing environment variable KEDA_HTTP_MIN_TLS_VERSION", "value", os.Getenv("KEDA_HTTP_MIN_TLS_VERSION"), "error", err.Error())
}
tlsCipherList = ParseTLSCipherList(os.Getenv("KEDA_HTTP_TLS_CIPHER_LIST"))

if envvar, found := os.LookupEnv("KEDA_SERVICE_MIN_TLS_VERSION"); found {
if serviceMinTLSVersion, err = ParseTLSVersion(envvar, tls.VersionTLS13); err != nil {
ctrl.Log.WithName("tls_setup").Info("Error parsing environment variable KEDA_HTTP_MIN_TLS_VERSION", "value", os.Getenv("KEDA_SERVICE_MIN_TLS_VERSION"), "error", err.Error())
}
} else {
serviceMinTLSVersion = minTLSVersion // fall back since the old behavior was for the webhook to use KEDA_HTTP_MIN_TLS_VERSION
}

if envvar, found := os.LookupEnv("KEDA_SERVICE_TLS_CIPHER_LIST"); found {
serviceTLSCipherList = ParseTLSCipherList(envvar)
} else {
serviceTLSCipherList = tlsCipherList
}
}

Expand Down Expand Up @@ -80,6 +99,7 @@ func CreateTLSClientConfig(unsafeSsl bool) *tls.Config {
InsecureSkipVerify: unsafeSsl,
RootCAs: getRootCAs(),
MinVersion: GetMinTLSVersion(),
CipherSuites: GetTLSCipherList(),
}
}

Expand All @@ -88,25 +108,63 @@ func GetMinTLSVersion() uint16 {
return minTLSVersion
}

func initMinTLSVersion() (uint16, error) {
version, _ := os.LookupEnv("KEDA_HTTP_MIN_TLS_VERSION")
// GetTLSCipherList returns the TLS cipher list based on configurations
func GetTLSCipherList() []uint16 {
return tlsCipherList
}

// GetServiceMinTLSVersion return the minimum TLS version that KEDA services are configured to use
func GetServiceMinTLSVersion() uint16 {
return serviceMinTLSVersion
}

// GetServiceTLSCipherList return the TLS ciphersuites that KEDA services are configured to use
func GetServiceTLSCipherList() []uint16 {
return serviceTLSCipherList
}

// ParseTLSCipherList parses a colon or comma-separated list of TLS cipher suite names
// (as returned by crypto/tls CipherSuites()) into a slice of cipher suite IDs.
// Unknown names are logged. Returns nil if no valid ciphers are found.
func ParseTLSCipherList(ciphers string) []uint16 {
reverseCipherMap := make(map[string]uint16)
for _, c := range tls.CipherSuites() {
reverseCipherMap[c.Name] = c.ID
}
var ciphersuites []uint16
for c := range strings.SplitSeq(strings.ReplaceAll(ciphers, ",", ":"), ":") {
c = strings.TrimSpace(c)
if id, ok := reverseCipherMap[c]; ok {
ciphersuites = append(ciphersuites, id)
} else {
ctrl.Log.WithName("tls_setup").Info("Unrecognized TLS ciphersuite name while parsing list of ciphersuites", "value", c)
}
Comment thread
joelsmith marked this conversation as resolved.
}
Comment thread
joelsmith marked this conversation as resolved.
if len(ciphersuites) == 0 {
return nil
}
return ciphersuites
}

// ParseTLSVersion converts a TLS version string to a TLS version value
func ParseTLSVersion(version string, defaultVersion uint16) (uint16, error) {
var ver uint16
switch version {
case "":
minTLSVersion = tls.VersionTLS12
ver = defaultVersion
case "TLS10":
minTLSVersion = tls.VersionTLS10
ver = tls.VersionTLS10
case "TLS11":
minTLSVersion = tls.VersionTLS11
ver = tls.VersionTLS11
case "TLS12":
minTLSVersion = tls.VersionTLS12
ver = tls.VersionTLS12
case "TLS13":
minTLSVersion = tls.VersionTLS13
ver = tls.VersionTLS13
default:
return tls.VersionTLS12, fmt.Errorf("%s is not a valid value, using `TLS12`. Allowed values are: `TLS13`,`TLS12`,`TLS11`,`TLS10`", version)
return defaultVersion, fmt.Errorf("%s is not a valid value, using `%s`. Allowed values are: `TLS13`,`TLS12`,`TLS11`,`TLS10`", version, strings.ReplaceAll(strings.ReplaceAll(tls.VersionName(defaultVersion), " ", ""), ".", ""))
}

return minTLSVersion, nil
return ver, nil
}

func decryptClientKey(clientKey, clientKeyPassword string) ([]byte, error) {
Expand Down
117 changes: 105 additions & 12 deletions pkg/util/tls_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,49 +254,142 @@ func TestNewTLSConfig_WithPassword(t *testing.T) {
}

type minTLSVersionTestData struct {
envSet bool
envValue string
expectedVersion uint16
}

var minTLSVersionTestDatas = []minTLSVersionTestData{
{
envSet: true,
envValue: "TLS10",
expectedVersion: tls.VersionTLS10,
},
{
envSet: true,
envValue: "TLS11",
expectedVersion: tls.VersionTLS11,
},
{
envSet: true,
envValue: "TLS12",
expectedVersion: tls.VersionTLS12,
},
{
envSet: true,
envValue: "TLS13",
expectedVersion: tls.VersionTLS13,
},
{
envSet: false,
envValue: "",
expectedVersion: tls.VersionTLS12,
},
}

func TestResolveMinTLSVersion(t *testing.T) {
defer os.Unsetenv("KEDA_HTTP_MIN_TLS_VERSION")
for _, testData := range minTLSVersionTestDatas {
os.Unsetenv("KEDA_HTTP_MIN_TLS_VERSION")
if testData.envSet {
os.Setenv("KEDA_HTTP_MIN_TLS_VERSION", testData.envValue)
}
minVersion, _ := initMinTLSVersion()
minVersion, _ := ParseTLSVersion(testData.envValue, tls.VersionTLS12)

if testData.expectedVersion != minVersion {
t.Error("Failed to resolve minTLSVersion correctly", "wants", testData.expectedVersion, "got", minVersion)
}
}
}

func TestParseTLSCipherList(t *testing.T) {
// Collect valid cipher names from the runtime for use in tests
allCiphers := tls.CipherSuites()
if len(allCiphers) == 0 {
t.Skip("no cipher suites available")
}
first := allCiphers[0]
second := allCiphers[len(allCiphers)-1]

tests := []struct {
name string
input string
wantNil bool
wantIDs []uint16
}{
{
name: "empty string returns nil",
input: "",
wantNil: true,
},
{
name: "unknown cipher names return nil",
input: "NOT_A_CIPHER:ALSO_NOT_A_CIPHER",
wantNil: true,
},
{
name: "single valid cipher colon-separated",
input: first.Name,
wantIDs: []uint16{first.ID},
},
{
name: "two valid ciphers colon-separated",
input: first.Name + ":" + second.Name,
wantIDs: []uint16{first.ID, second.ID},
},
{
name: "two valid ciphers comma-separated",
input: first.Name + "," + second.Name,
wantIDs: []uint16{first.ID, second.ID},
},
{
name: "valid cipher mixed with unknown cipher",
input: first.Name + ":NOT_A_CIPHER",
wantIDs: []uint16{first.ID},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseTLSCipherList(tt.input)
if tt.wantNil {
if got != nil {
t.Errorf("expected nil, got %v", got)
}
return
}
if len(got) != len(tt.wantIDs) {
t.Fatalf("expected %d cipher(s), got %d: %v", len(tt.wantIDs), len(got), got)
}
for i, id := range tt.wantIDs {
if got[i] != id {
t.Errorf("cipher[%d]: expected 0x%04x, got 0x%04x", i, id, got[i])
}
}
})
}
}

func TestParseTLSCipherListEnvVar(t *testing.T) {
allCiphers := tls.CipherSuites()
if len(allCiphers) == 0 {
t.Skip("no cipher suites available")
}
first := allCiphers[0]

t.Run("env var unset returns nil", func(t *testing.T) {
os.Unsetenv("KEDA_HTTP_TLS_CIPHER_LIST")
if got := ParseTLSCipherList(os.Getenv("KEDA_HTTP_TLS_CIPHER_LIST")); got != nil {
t.Errorf("expected nil, got %v", got)
}
})

t.Run("env var set returns ciphers", func(t *testing.T) {
os.Setenv("KEDA_HTTP_TLS_CIPHER_LIST", first.Name)
defer os.Unsetenv("KEDA_HTTP_TLS_CIPHER_LIST")
got := ParseTLSCipherList(os.Getenv("KEDA_HTTP_TLS_CIPHER_LIST"))
if len(got) != 1 || got[0] != first.ID {
t.Errorf("expected [0x%04x], got %v", first.ID, got)
}
})

t.Run("all available ciphers colon-separated", func(t *testing.T) {
names := make([]string, len(allCiphers))
for i, c := range allCiphers {
names[i] = c.Name
}
got := ParseTLSCipherList(strings.Join(names, ":"))
if len(got) != len(allCiphers) {
t.Errorf("expected %d ciphers, got %d", len(allCiphers), len(got))
}
})
}
Loading