Skip to content

Commit

Permalink
Merge branch 'v1.18.x' into eitanya/backport-7504
Browse files Browse the repository at this point in the history
  • Loading branch information
EItanya authored Jan 10, 2025
2 parents 74b9296 + cdaa04d commit a4d2075
Show file tree
Hide file tree
Showing 17 changed files with 670 additions and 41 deletions.
9 changes: 9 additions & 0 deletions changelog/v1.18.4/7505-gw-api-cipher-suites.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

changelog:
- type: FIX
issueLink: https://github.com/solo-io/solo-projects/issues/7505
resolvesIssue: true
description: >
Add new SSL options to GatewayTLSConfig to enable configuring additional SSL options
which were previously available using the edge API. This includes cipher suites, minimum TLS version,
maximum TLS version, client certificate validation, and one way TLS.
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ func translateSslConfig(
if sniDomain != nil {
sniDomains = []string{string(*sniDomain)}
}
return &ssl.SslConfig{
cfg := &ssl.SslConfig{
SslSecrets: &ssl.SslConfig_SecretRef{SecretRef: secretRef},
SniDomains: sniDomains,
VerifySubjectAltName: nil,
Expand All @@ -760,7 +760,12 @@ func translateSslConfig(
DisableTlsSessionResumption: nil,
TransportSocketConnectTimeout: nil,
OcspStaplePolicy: 0,
}, nil
}

// Apply known SSL Extension options
sslutils.ApplySslExtensionOptions(ctx, tls, cfg)

return cfg, nil
}

// makeVhostName computes the name of a virtual host based on the parent name and domain.
Expand Down
111 changes: 111 additions & 0 deletions projects/gateway2/translator/sslutils/ssl_utils.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
package sslutils

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

"github.com/hashicorp/go-multierror"
"github.com/rotisserie/eris"
"github.com/solo-io/gloo/projects/gateway2/wellknown"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/ssl"
"github.com/solo-io/go-utils/contextutils"
"google.golang.org/protobuf/types/known/wrapperspb"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/util/cert"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// Gateway API has an extension point for implementation specific tls settings, they can be found [here](https://gateway-api.sigs.k8s.io/guides/tls/#extensions)
const (
GatewaySslOptionsPrefix = wellknown.GatewayAnnotationPrefix + "/ssl"

GatewaySslCipherSuites = GatewaySslOptionsPrefix + "/cipher-suites"
GatewaySslMinimumTlsVersion = GatewaySslOptionsPrefix + "/minimum-tls-version"
GatewaySslMaximumTlsVersion = GatewaySslOptionsPrefix + "/maximum-tls-version"
GatewaySslOneWayTls = GatewaySslOptionsPrefix + "/one-way-tls"
GatewaySslVerifySubjectAltName = GatewaySslOptionsPrefix + "/verify-subject-alt-name"
)

var (
Expand Down Expand Up @@ -59,3 +78,95 @@ func cleanedSslKeyPair(certChain, privateKey, rootCa string) (cleanedChain strin

return cleanedChain, err
}

type SslExtensionOptionFunc = func(ctx context.Context, in string, out *ssl.SslConfig) error

func ApplyCipherSuites(ctx context.Context, in string, out *ssl.SslConfig) error {
if out.GetParameters() == nil {
out.Parameters = &ssl.SslParameters{}
}
cipherSuites := strings.Split(in, ",")
out.GetParameters().CipherSuites = cipherSuites
return nil
}

func ApplyMinimumTlsVersion(ctx context.Context, in string, out *ssl.SslConfig) error {
if out.GetParameters() == nil {
out.Parameters = &ssl.SslParameters{}
}
if parsed, ok := ssl.SslParameters_ProtocolVersion_value[in]; ok {
out.GetParameters().MinimumProtocolVersion = ssl.SslParameters_ProtocolVersion(parsed)
if out.GetParameters().GetMaximumProtocolVersion() != ssl.SslParameters_TLS_AUTO && out.GetParameters().GetMaximumProtocolVersion() < out.GetParameters().GetMinimumProtocolVersion() {
err := eris.Errorf("maximum tls version %s is less than minimum tls version %s", out.GetParameters().GetMaximumProtocolVersion().String(), in)
out.GetParameters().MaximumProtocolVersion = ssl.SslParameters_TLS_AUTO
out.GetParameters().MinimumProtocolVersion = ssl.SslParameters_TLS_AUTO
return err
}
} else {
return eris.Errorf("invalid minimum tls version: %s", in)
}
return nil
}

func ApplyMaximumTlsVersion(ctx context.Context, in string, out *ssl.SslConfig) error {
if out.GetParameters() == nil {
out.Parameters = &ssl.SslParameters{}
}
if parsed, ok := ssl.SslParameters_ProtocolVersion_value[in]; ok {
out.GetParameters().MaximumProtocolVersion = ssl.SslParameters_ProtocolVersion(parsed)
if out.GetParameters().GetMaximumProtocolVersion() != ssl.SslParameters_TLS_AUTO && out.GetParameters().GetMaximumProtocolVersion() < out.GetParameters().GetMinimumProtocolVersion() {
err := eris.Errorf("maximum tls version %s is less than minimum tls version %s", in, out.GetParameters().GetMinimumProtocolVersion().String())
out.GetParameters().MaximumProtocolVersion = ssl.SslParameters_TLS_AUTO
out.GetParameters().MinimumProtocolVersion = ssl.SslParameters_TLS_AUTO
return err
}
} else {
return eris.Errorf("invalid maximum tls version: %s", in)
}
return nil
}

func ApplyOneWayTls(ctx context.Context, in string, out *ssl.SslConfig) error {
if strings.ToLower(in) == "true" {
out.OneWayTls = wrapperspb.Bool(true)
} else if strings.ToLower(in) == "false" {
out.OneWayTls = wrapperspb.Bool(false)
} else {
return eris.Errorf("invalid value for one-way-tls: %s", in)
}
return nil
}

func ApplyVerifySubjectAltName(ctx context.Context, in string, out *ssl.SslConfig) error {
altNames := strings.Split(in, ",")
out.VerifySubjectAltName = altNames
return nil
}

var SslExtensionOptionFuncs = map[string]SslExtensionOptionFunc{
GatewaySslCipherSuites: ApplyCipherSuites,
GatewaySslMinimumTlsVersion: ApplyMinimumTlsVersion,
GatewaySslMaximumTlsVersion: ApplyMaximumTlsVersion,
GatewaySslOneWayTls: ApplyOneWayTls,
GatewaySslVerifySubjectAltName: ApplyVerifySubjectAltName,
}

// ApplySslExtensionOptions applies the GatewayTLSConfig options to the SslConfig
// This function will never exit early, even if an error is encountered.
// It will apply all options and log all errors encountered.
func ApplySslExtensionOptions(ctx context.Context, in *gwv1.GatewayTLSConfig, out *ssl.SslConfig) {
var wrapped error
for key, option := range in.Options {
if extensionFunc, ok := SslExtensionOptionFuncs[string(key)]; ok {
if err := extensionFunc(ctx, string(option), out); err != nil {
wrapped = multierror.Append(wrapped, err)
}
} else {
wrapped = multierror.Append(wrapped, eris.Errorf("unknown ssl option: %s", key))
}
}

if wrapped != nil {
contextutils.LoggerFrom(ctx).Warnf("error applying ssl extension options: %v", wrapped)
}
}
221 changes: 221 additions & 0 deletions projects/gateway2/translator/sslutils/ssl_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package sslutils

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/ssl"
"github.com/solo-io/go-utils/contextutils"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/wrapperspb"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

func TestApplySslExtensionOptions(t *testing.T) {
testCases := []struct {
name string
out *ssl.SslConfig
in *gwv1.GatewayTLSConfig
errors []string
}{
{
name: "one_way_tls_true",
out: &ssl.SslConfig{
OneWayTls: wrapperspb.Bool(true),
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslOneWayTls: "true",
},
},
},
{
name: "one_way_tls_true_incorrect_casing",
out: &ssl.SslConfig{
OneWayTls: wrapperspb.Bool(true),
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslOneWayTls: "True",
},
},
},
{
name: "one_way_tls_false",
out: &ssl.SslConfig{
OneWayTls: wrapperspb.Bool(false),
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslOneWayTls: "false",
},
},
},
{
name: "one_way_tls_false_incorrect_casing",
out: &ssl.SslConfig{
OneWayTls: wrapperspb.Bool(false),
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslOneWayTls: "False",
},
},
},
{
name: "invalid_one_way_tls",
out: &ssl.SslConfig{},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslOneWayTls: "Foo",
},
},
errors: []string{"invalid value for one-way-tls: Foo"},
},
{
name: "cipher_suites",
out: &ssl.SslConfig{
Parameters: &ssl.SslParameters{
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
},
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
{
name: "subject_alt_names",
out: &ssl.SslConfig{
VerifySubjectAltName: []string{"foo", "bar"},
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslVerifySubjectAltName: "foo,bar",
},
},
},
{
name: "tls_max_version",
out: &ssl.SslConfig{
Parameters: &ssl.SslParameters{
MaximumProtocolVersion: ssl.SslParameters_TLSv1_2,
},
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslMaximumTlsVersion: "TLSv1_2",
},
},
},
{
name: "tls_min_version",
out: &ssl.SslConfig{
Parameters: &ssl.SslParameters{
MinimumProtocolVersion: ssl.SslParameters_TLSv1_3,
},
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslMinimumTlsVersion: "TLSv1_3",
},
},
},
{
name: "invalid_tls_versions",
out: &ssl.SslConfig{
Parameters: &ssl.SslParameters{},
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslMinimumTlsVersion: "TLSv1.3",
GatewaySslMaximumTlsVersion: "TLSv1.2",
},
},
errors: []string{
"invalid maximum tls version: TLSv1.2",
"invalid minimum tls version: TLSv1.3",
},
},
{
name: "maximium_tls_version_less_than_minimum",
out: &ssl.SslConfig{
VerifySubjectAltName: []string{"foo", "bar"},
Parameters: &ssl.SslParameters{},
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslMinimumTlsVersion: "TLSv1_3",
GatewaySslMaximumTlsVersion: "TLSv1_2",
GatewaySslVerifySubjectAltName: "foo,bar",
},
},
errors: []string{
"maximum tls version TLSv1_2 is less than minimum tls version TLSv1_3",
},
},
{
name: "multiple_options",
out: &ssl.SslConfig{
VerifySubjectAltName: []string{"foo", "bar"},
OneWayTls: wrapperspb.Bool(true),
Parameters: &ssl.SslParameters{
MaximumProtocolVersion: ssl.SslParameters_TLSv1_3,
MinimumProtocolVersion: ssl.SslParameters_TLSv1_2,
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
},
},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslMaximumTlsVersion: "TLSv1_3",
GatewaySslMinimumTlsVersion: "TLSv1_2",
GatewaySslVerifySubjectAltName: "foo,bar",
GatewaySslOneWayTls: "true",
GatewaySslCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
{
name: "misspelled_option",
out: &ssl.SslConfig{},
in: &gwv1.GatewayTLSConfig{
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
GatewaySslMinimumTlsVersion + "s": "TLSv1_3",
},
},
errors: []string{
"unknown ssl option: gateway.gloo.solo.io/ssl/minimum-tls-versions",
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := &zaptest.Buffer{}
logger := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
b,
zapcore.DebugLevel,
))
ctx := contextutils.WithExistingLogger(context.Background(), logger.Sugar())
out := &ssl.SslConfig{}
ApplySslExtensionOptions(ctx, tc.in, out)
assert.Empty(t, cmp.Diff(tc.out, out, protocmp.Transform()))
if len(tc.errors) > 0 {
assert.Contains(t, b.String(), "error applying ssl extension options")
for _, err := range tc.errors {
assert.Contains(t, b.String(), err)
}
} else {
assert.Empty(t, b.String())
}
})

}
}
5 changes: 4 additions & 1 deletion projects/gateway2/wellknown/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ const (
// It is configured to manage GatewayClasses with the name GatewayClassName
GatewayControllerName = "solo.io/gloo-gateway"

// GatewayAnnotationPrefix is the prefix for all Gateway annotations/options
GatewayAnnotationPrefix = "gateway.gloo.solo.io"

// GatewayParametersAnnotationName is the name of the Gateway annotation that specifies
// the name of a GatewayParameters CR, which is used to dynamically provision the data plane
// resources for the Gateway. The GatewayParameters is assumed to be in the same namespace
// as the Gateway.
GatewayParametersAnnotationName = "gateway.gloo.solo.io/gateway-parameters-name"
GatewayParametersAnnotationName = GatewayAnnotationPrefix + "/gateway-parameters-name"

// DefaultGatewayParametersName is the name of the GatewayParameters which is attached by
// parametersRef to the GatewayClass.
Expand Down
Loading

0 comments on commit a4d2075

Please sign in to comment.