Skip to content

Commit 3a3e707

Browse files
EItanyanfudenarianaw66
authored
Gateway SSL Extensions (#10553)
Co-authored-by: Nathan Fudenberg <[email protected]> Co-authored-by: Ariana W. <[email protected]>
1 parent ca93dd0 commit 3a3e707

18 files changed

+671
-42
lines changed

changelog/v1.19.0-beta3/7504-gwapi-static-node-ports.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ changelog:
66
description: >
77
Add a new Ports field to the `GatewayParameters` `Kube.Service` Spec in order to allow admin users
88
to configure additional information about the ports that the Gateway should listen on. This is useful
9-
if the user wants to specify a static `NodePort`
9+
if the user wants to specify a static `NodePort`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
changelog:
3+
- type: NEW_FEATURE
4+
issueLink: https://github.com/solo-io/solo-projects/issues/7505
5+
resolvesIssue: true
6+
description: >
7+
Add new SSL options to GatewayTLSConfig to enable configuring additional SSL options
8+
which were previously available using the edge API. This includes cipher suites, minimum TLS version,
9+
maximum TLS version, client certificate validation, and one way TLS.

projects/gateway2/translator/listener/gateway_listener_translator.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ func translateSslConfig(
765765
if sniDomain != nil {
766766
sniDomains = []string{string(*sniDomain)}
767767
}
768-
return &ssl.SslConfig{
768+
cfg := &ssl.SslConfig{
769769
SslSecrets: &ssl.SslConfig_SecretRef{SecretRef: secretRef},
770770
SniDomains: sniDomains,
771771
VerifySubjectAltName: nil,
@@ -775,7 +775,12 @@ func translateSslConfig(
775775
DisableTlsSessionResumption: nil,
776776
TransportSocketConnectTimeout: nil,
777777
OcspStaplePolicy: 0,
778-
}, nil
778+
}
779+
780+
// Apply known SSL Extension options
781+
sslutils.ApplySslExtensionOptions(ctx, tls, cfg)
782+
783+
return cfg, nil
779784
}
780785

781786
// makeVhostName computes the name of a virtual host based on the parent name and domain.

projects/gateway2/translator/sslutils/ssl_utils.go

+111
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
11
package sslutils
22

33
import (
4+
"context"
45
"crypto/tls"
56
"fmt"
7+
"strings"
68

9+
"github.com/hashicorp/go-multierror"
710
"github.com/rotisserie/eris"
11+
"github.com/solo-io/gloo/projects/gateway2/wellknown"
12+
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/ssl"
13+
"github.com/solo-io/go-utils/contextutils"
14+
"google.golang.org/protobuf/types/known/wrapperspb"
815
corev1 "k8s.io/api/core/v1"
916
"k8s.io/client-go/util/cert"
17+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
18+
)
19+
20+
// 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)
21+
const (
22+
GatewaySslOptionsPrefix = wellknown.GatewayAnnotationPrefix + "/ssl"
23+
24+
GatewaySslCipherSuites = GatewaySslOptionsPrefix + "/cipher-suites"
25+
GatewaySslMinimumTlsVersion = GatewaySslOptionsPrefix + "/minimum-tls-version"
26+
GatewaySslMaximumTlsVersion = GatewaySslOptionsPrefix + "/maximum-tls-version"
27+
GatewaySslOneWayTls = GatewaySslOptionsPrefix + "/one-way-tls"
28+
GatewaySslVerifySubjectAltName = GatewaySslOptionsPrefix + "/verify-subject-alt-name"
1029
)
1130

1231
var (
@@ -59,3 +78,95 @@ func cleanedSslKeyPair(certChain, privateKey, rootCa string) (cleanedChain strin
5978

6079
return cleanedChain, err
6180
}
81+
82+
type SslExtensionOptionFunc = func(ctx context.Context, in string, out *ssl.SslConfig) error
83+
84+
func ApplyCipherSuites(ctx context.Context, in string, out *ssl.SslConfig) error {
85+
if out.GetParameters() == nil {
86+
out.Parameters = &ssl.SslParameters{}
87+
}
88+
cipherSuites := strings.Split(in, ",")
89+
out.GetParameters().CipherSuites = cipherSuites
90+
return nil
91+
}
92+
93+
func ApplyMinimumTlsVersion(ctx context.Context, in string, out *ssl.SslConfig) error {
94+
if out.GetParameters() == nil {
95+
out.Parameters = &ssl.SslParameters{}
96+
}
97+
if parsed, ok := ssl.SslParameters_ProtocolVersion_value[in]; ok {
98+
out.GetParameters().MinimumProtocolVersion = ssl.SslParameters_ProtocolVersion(parsed)
99+
if out.GetParameters().GetMaximumProtocolVersion() != ssl.SslParameters_TLS_AUTO && out.GetParameters().GetMaximumProtocolVersion() < out.GetParameters().GetMinimumProtocolVersion() {
100+
err := eris.Errorf("maximum tls version %s is less than minimum tls version %s", out.GetParameters().GetMaximumProtocolVersion().String(), in)
101+
out.GetParameters().MaximumProtocolVersion = ssl.SslParameters_TLS_AUTO
102+
out.GetParameters().MinimumProtocolVersion = ssl.SslParameters_TLS_AUTO
103+
return err
104+
}
105+
} else {
106+
return eris.Errorf("invalid minimum tls version: %s", in)
107+
}
108+
return nil
109+
}
110+
111+
func ApplyMaximumTlsVersion(ctx context.Context, in string, out *ssl.SslConfig) error {
112+
if out.GetParameters() == nil {
113+
out.Parameters = &ssl.SslParameters{}
114+
}
115+
if parsed, ok := ssl.SslParameters_ProtocolVersion_value[in]; ok {
116+
out.GetParameters().MaximumProtocolVersion = ssl.SslParameters_ProtocolVersion(parsed)
117+
if out.GetParameters().GetMaximumProtocolVersion() != ssl.SslParameters_TLS_AUTO && out.GetParameters().GetMaximumProtocolVersion() < out.GetParameters().GetMinimumProtocolVersion() {
118+
err := eris.Errorf("maximum tls version %s is less than minimum tls version %s", in, out.GetParameters().GetMinimumProtocolVersion().String())
119+
out.GetParameters().MaximumProtocolVersion = ssl.SslParameters_TLS_AUTO
120+
out.GetParameters().MinimumProtocolVersion = ssl.SslParameters_TLS_AUTO
121+
return err
122+
}
123+
} else {
124+
return eris.Errorf("invalid maximum tls version: %s", in)
125+
}
126+
return nil
127+
}
128+
129+
func ApplyOneWayTls(ctx context.Context, in string, out *ssl.SslConfig) error {
130+
if strings.ToLower(in) == "true" {
131+
out.OneWayTls = wrapperspb.Bool(true)
132+
} else if strings.ToLower(in) == "false" {
133+
out.OneWayTls = wrapperspb.Bool(false)
134+
} else {
135+
return eris.Errorf("invalid value for one-way-tls: %s", in)
136+
}
137+
return nil
138+
}
139+
140+
func ApplyVerifySubjectAltName(ctx context.Context, in string, out *ssl.SslConfig) error {
141+
altNames := strings.Split(in, ",")
142+
out.VerifySubjectAltName = altNames
143+
return nil
144+
}
145+
146+
var SslExtensionOptionFuncs = map[string]SslExtensionOptionFunc{
147+
GatewaySslCipherSuites: ApplyCipherSuites,
148+
GatewaySslMinimumTlsVersion: ApplyMinimumTlsVersion,
149+
GatewaySslMaximumTlsVersion: ApplyMaximumTlsVersion,
150+
GatewaySslOneWayTls: ApplyOneWayTls,
151+
GatewaySslVerifySubjectAltName: ApplyVerifySubjectAltName,
152+
}
153+
154+
// ApplySslExtensionOptions applies the GatewayTLSConfig options to the SslConfig
155+
// This function will never exit early, even if an error is encountered.
156+
// It will apply all options and log all errors encountered.
157+
func ApplySslExtensionOptions(ctx context.Context, in *gwv1.GatewayTLSConfig, out *ssl.SslConfig) {
158+
var wrapped error
159+
for key, option := range in.Options {
160+
if extensionFunc, ok := SslExtensionOptionFuncs[string(key)]; ok {
161+
if err := extensionFunc(ctx, string(option), out); err != nil {
162+
wrapped = multierror.Append(wrapped, err)
163+
}
164+
} else {
165+
wrapped = multierror.Append(wrapped, eris.Errorf("unknown ssl option: %s", key))
166+
}
167+
}
168+
169+
if wrapped != nil {
170+
contextutils.LoggerFrom(ctx).Warnf("error applying ssl extension options: %v", wrapped)
171+
}
172+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package sslutils
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/ssl"
9+
"github.com/solo-io/go-utils/contextutils"
10+
"github.com/stretchr/testify/assert"
11+
"go.uber.org/zap"
12+
"go.uber.org/zap/zapcore"
13+
"go.uber.org/zap/zaptest"
14+
"google.golang.org/protobuf/testing/protocmp"
15+
"google.golang.org/protobuf/types/known/wrapperspb"
16+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
17+
)
18+
19+
func TestApplySslExtensionOptions(t *testing.T) {
20+
testCases := []struct {
21+
name string
22+
out *ssl.SslConfig
23+
in *gwv1.GatewayTLSConfig
24+
errors []string
25+
}{
26+
{
27+
name: "one_way_tls_true",
28+
out: &ssl.SslConfig{
29+
OneWayTls: wrapperspb.Bool(true),
30+
},
31+
in: &gwv1.GatewayTLSConfig{
32+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
33+
GatewaySslOneWayTls: "true",
34+
},
35+
},
36+
},
37+
{
38+
name: "one_way_tls_true_incorrect_casing",
39+
out: &ssl.SslConfig{
40+
OneWayTls: wrapperspb.Bool(true),
41+
},
42+
in: &gwv1.GatewayTLSConfig{
43+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
44+
GatewaySslOneWayTls: "True",
45+
},
46+
},
47+
},
48+
{
49+
name: "one_way_tls_false",
50+
out: &ssl.SslConfig{
51+
OneWayTls: wrapperspb.Bool(false),
52+
},
53+
in: &gwv1.GatewayTLSConfig{
54+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
55+
GatewaySslOneWayTls: "false",
56+
},
57+
},
58+
},
59+
{
60+
name: "one_way_tls_false_incorrect_casing",
61+
out: &ssl.SslConfig{
62+
OneWayTls: wrapperspb.Bool(false),
63+
},
64+
in: &gwv1.GatewayTLSConfig{
65+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
66+
GatewaySslOneWayTls: "False",
67+
},
68+
},
69+
},
70+
{
71+
name: "invalid_one_way_tls",
72+
out: &ssl.SslConfig{},
73+
in: &gwv1.GatewayTLSConfig{
74+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
75+
GatewaySslOneWayTls: "Foo",
76+
},
77+
},
78+
errors: []string{"invalid value for one-way-tls: Foo"},
79+
},
80+
{
81+
name: "cipher_suites",
82+
out: &ssl.SslConfig{
83+
Parameters: &ssl.SslParameters{
84+
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
85+
},
86+
},
87+
in: &gwv1.GatewayTLSConfig{
88+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
89+
GatewaySslCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
90+
},
91+
},
92+
},
93+
{
94+
name: "subject_alt_names",
95+
out: &ssl.SslConfig{
96+
VerifySubjectAltName: []string{"foo", "bar"},
97+
},
98+
in: &gwv1.GatewayTLSConfig{
99+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
100+
GatewaySslVerifySubjectAltName: "foo,bar",
101+
},
102+
},
103+
},
104+
{
105+
name: "tls_max_version",
106+
out: &ssl.SslConfig{
107+
Parameters: &ssl.SslParameters{
108+
MaximumProtocolVersion: ssl.SslParameters_TLSv1_2,
109+
},
110+
},
111+
in: &gwv1.GatewayTLSConfig{
112+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
113+
GatewaySslMaximumTlsVersion: "TLSv1_2",
114+
},
115+
},
116+
},
117+
{
118+
name: "tls_min_version",
119+
out: &ssl.SslConfig{
120+
Parameters: &ssl.SslParameters{
121+
MinimumProtocolVersion: ssl.SslParameters_TLSv1_3,
122+
},
123+
},
124+
in: &gwv1.GatewayTLSConfig{
125+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
126+
GatewaySslMinimumTlsVersion: "TLSv1_3",
127+
},
128+
},
129+
},
130+
{
131+
name: "invalid_tls_versions",
132+
out: &ssl.SslConfig{
133+
Parameters: &ssl.SslParameters{},
134+
},
135+
in: &gwv1.GatewayTLSConfig{
136+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
137+
GatewaySslMinimumTlsVersion: "TLSv1.3",
138+
GatewaySslMaximumTlsVersion: "TLSv1.2",
139+
},
140+
},
141+
errors: []string{
142+
"invalid maximum tls version: TLSv1.2",
143+
"invalid minimum tls version: TLSv1.3",
144+
},
145+
},
146+
{
147+
name: "maximium_tls_version_less_than_minimum",
148+
out: &ssl.SslConfig{
149+
VerifySubjectAltName: []string{"foo", "bar"},
150+
Parameters: &ssl.SslParameters{},
151+
},
152+
in: &gwv1.GatewayTLSConfig{
153+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
154+
GatewaySslMinimumTlsVersion: "TLSv1_3",
155+
GatewaySslMaximumTlsVersion: "TLSv1_2",
156+
GatewaySslVerifySubjectAltName: "foo,bar",
157+
},
158+
},
159+
errors: []string{
160+
"maximum tls version TLSv1_2 is less than minimum tls version TLSv1_3",
161+
},
162+
},
163+
{
164+
name: "multiple_options",
165+
out: &ssl.SslConfig{
166+
VerifySubjectAltName: []string{"foo", "bar"},
167+
OneWayTls: wrapperspb.Bool(true),
168+
Parameters: &ssl.SslParameters{
169+
MaximumProtocolVersion: ssl.SslParameters_TLSv1_3,
170+
MinimumProtocolVersion: ssl.SslParameters_TLSv1_2,
171+
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
172+
},
173+
},
174+
in: &gwv1.GatewayTLSConfig{
175+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
176+
GatewaySslMaximumTlsVersion: "TLSv1_3",
177+
GatewaySslMinimumTlsVersion: "TLSv1_2",
178+
GatewaySslVerifySubjectAltName: "foo,bar",
179+
GatewaySslOneWayTls: "true",
180+
GatewaySslCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
181+
},
182+
},
183+
},
184+
{
185+
name: "misspelled_option",
186+
out: &ssl.SslConfig{},
187+
in: &gwv1.GatewayTLSConfig{
188+
Options: map[gwv1.AnnotationKey]gwv1.AnnotationValue{
189+
GatewaySslMinimumTlsVersion + "s": "TLSv1_3",
190+
},
191+
},
192+
errors: []string{
193+
"unknown ssl option: gateway.gloo.solo.io/ssl/minimum-tls-versions",
194+
},
195+
},
196+
}
197+
198+
for _, tc := range testCases {
199+
t.Run(tc.name, func(t *testing.T) {
200+
b := &zaptest.Buffer{}
201+
logger := zap.New(zapcore.NewCore(
202+
zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
203+
b,
204+
zapcore.DebugLevel,
205+
))
206+
ctx := contextutils.WithExistingLogger(context.Background(), logger.Sugar())
207+
out := &ssl.SslConfig{}
208+
ApplySslExtensionOptions(ctx, tc.in, out)
209+
assert.Empty(t, cmp.Diff(tc.out, out, protocmp.Transform()))
210+
if len(tc.errors) > 0 {
211+
assert.Contains(t, b.String(), "error applying ssl extension options")
212+
for _, err := range tc.errors {
213+
assert.Contains(t, b.String(), err)
214+
}
215+
} else {
216+
assert.Empty(t, b.String())
217+
}
218+
})
219+
220+
}
221+
}

0 commit comments

Comments
 (0)