Skip to content

Commit 90622af

Browse files
committed
Support multiple server certificates in gateway (istio & gateway api)
Address istio#36181 - Update istio gateway to support multiple server certificates (istio/api#3466) - Update gateway api support to allow at most 2 server certificates
1 parent 35d4931 commit 90622af

File tree

11 files changed

+691
-89
lines changed

11 files changed

+691
-89
lines changed

pilot/pkg/config/kube/gateway/conversion.go

+22-17
Original file line numberDiff line numberDiff line change
@@ -1906,26 +1906,31 @@ func buildTLS(
19061906
return out, nil
19071907
}
19081908
}
1909-
if len(tls.CertificateRefs) != 1 {
1910-
// This is required in the API, should be rejected in validation
1911-
return out, &ConfigError{Reason: InvalidTLS, Message: "exactly 1 certificateRefs should be present for TLS termination"}
1912-
}
1913-
cred, err := buildSecretReference(ctx, tls.CertificateRefs[0], gw, secrets)
1914-
if err != nil {
1915-
return out, err
1916-
}
1917-
credNs := ptr.OrDefault((*string)(tls.CertificateRefs[0].Namespace), namespace)
1918-
sameNamespace := credNs == namespace
1919-
if !sameNamespace && !grants.SecretAllowed(ctx, creds.ToResourceName(cred), namespace) {
1909+
if len(tls.CertificateRefs) > 2 {
19201910
return out, &ConfigError{
1921-
Reason: InvalidListenerRefNotPermitted,
1922-
Message: fmt.Sprintf(
1923-
"certificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)",
1924-
tls.CertificateRefs[0].Name, credNs, namespace,
1925-
),
1911+
Reason: InvalidTLS,
1912+
Message: "TLS mode can only support up to 2 server certificates",
1913+
}
1914+
}
1915+
out.CredentialNames = make([]string, len(tls.CertificateRefs))
1916+
for i, certRef := range tls.CertificateRefs {
1917+
cred, err := buildSecretReference(ctx, certRef, gw, secrets)
1918+
if err != nil {
1919+
return out, err
1920+
}
1921+
credNs := ptr.OrDefault((*string)(certRef.Namespace), namespace)
1922+
sameNamespace := credNs == namespace
1923+
if !sameNamespace && !grants.SecretAllowed(ctx, creds.ToResourceName(cred), namespace) {
1924+
return out, &ConfigError{
1925+
Reason: InvalidListenerRefNotPermitted,
1926+
Message: fmt.Sprintf(
1927+
"certificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)",
1928+
tls.CertificateRefs[0].Name, credNs, namespace,
1929+
),
1930+
}
19261931
}
1932+
out.CredentialNames[i] = cred
19271933
}
1928-
out.CredentialName = cred
19291934
case k8s.TLSModePassthrough:
19301935
out.Mode = istio.ServerTLSSettings_PASSTHROUGH
19311936
if isAutoPassthrough {

pilot/pkg/config/kube/gateway/testdata/reference-policy-tls.yaml.golden

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ spec:
1717
number: 443
1818
protocol: HTTPS
1919
tls:
20-
credentialName: kubernetes-gateway://cert/cert
20+
credentialNames:
21+
- kubernetes-gateway://cert/cert
2122
mode: SIMPLE
2223
---
2324
apiVersion: networking.istio.io/v1

pilot/pkg/config/kube/gateway/testdata/tls.yaml.golden

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ spec:
3737
number: 34000
3838
protocol: HTTPS
3939
tls:
40-
credentialName: kubernetes-gateway://istio-system/my-cert-http
40+
credentialNames:
41+
- kubernetes-gateway://istio-system/my-cert-http
4142
mode: SIMPLE
4243
---
4344
apiVersion: networking.istio.io/v1
@@ -80,7 +81,8 @@ spec:
8081
number: 34000
8182
protocol: HTTPS
8283
tls:
83-
credentialName: kubernetes-gateway://istio-system/my-cert-http
84+
credentialNames:
85+
- kubernetes-gateway://istio-system/my-cert-http
8486
mode: MUTUAL
8587
---
8688
apiVersion: networking.istio.io/v1

pilot/pkg/model/context.go

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type (
6666
PodPort = pm.PodPort
6767
StringBool = pm.StringBool
6868
IPMode = pm.IPMode
69+
TLSServerCertificate = pm.TLSServerCertificate
6970
)
7071

7172
const (

pilot/pkg/networking/core/listener.go

+27-5
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,40 @@ func BuildListenerTLSContext(serverTLSSettings *networking.ServerTLSSettings,
164164
switch {
165165
case serverTLSSettings.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL:
166166
authnmodel.ApplyToCommonTLSContext(ctx.CommonTlsContext, proxy, serverTLSSettings.SubjectAltNames, serverTLSSettings.CaCrl, []string{}, validateClient)
167+
// If credential names are specified at gateway config, create SDS config for gateway to fetch key/cert from Istiod.
168+
case len(serverTLSSettings.GetCredentialNames()) > 0:
169+
authnmodel.ApplyCredentialSDSToServerCommonTLSContext(ctx.CommonTlsContext, serverTLSSettings, credentialSocketExist)
167170
// If credential name is specified at gateway config, create SDS config for gateway to fetch key/cert from Istiod.
168171
case serverTLSSettings.CredentialName != "":
169172
authnmodel.ApplyCredentialSDSToServerCommonTLSContext(ctx.CommonTlsContext, serverTLSSettings, credentialSocketExist)
170173
default:
174+
// If certificate files are specified in gateway configuration, use file based SDS.
175+
var tlsCertificates []*model.TLSServerCertificate
176+
// If multiple certificates are specified in gateway configuration, create proxy with multiple certificates.
177+
if len(serverTLSSettings.TlsCertificates) > 0 {
178+
tlsCertificates = make([]*model.TLSServerCertificate, len(serverTLSSettings.TlsCertificates))
179+
for i, cert := range serverTLSSettings.TlsCertificates {
180+
tlsCertificates[i] = &model.TLSServerCertificate{
181+
TLSServerCertChain: cert.ServerCertificate,
182+
TLSServerKey: cert.PrivateKey,
183+
TLSServerRootCert: cert.CaCertificates,
184+
}
185+
}
186+
// Fallback to single certificate via server certificate settings in the gateway config.
187+
} else {
188+
tlsCertificates = []*model.TLSServerCertificate{
189+
{
190+
TLSServerCertChain: serverTLSSettings.ServerCertificate,
191+
TLSServerKey: serverTLSSettings.PrivateKey,
192+
TLSServerRootCert: serverTLSSettings.CaCertificates,
193+
},
194+
}
195+
}
171196
certProxy := &model.Proxy{}
172197
certProxy.IstioVersion = proxy.IstioVersion
173-
// If certificate files are specified in gateway configuration, use file based SDS.
174198
certProxy.Metadata = &model.NodeMetadata{
175-
TLSServerCertChain: serverTLSSettings.ServerCertificate,
176-
TLSServerKey: serverTLSSettings.PrivateKey,
177-
TLSServerRootCert: serverTLSSettings.CaCertificates,
178-
Raw: proxy.Metadata.Raw,
199+
TLSServerCertificates: tlsCertificates,
200+
Raw: proxy.Metadata.Raw,
179201
}
180202

181203
authnmodel.ApplyToCommonTLSContext(ctx.CommonTlsContext, certProxy, serverTLSSettings.SubjectAltNames, serverTLSSettings.CaCrl, []string{}, validateClient)

pilot/pkg/networking/core/listener_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ import (
3535
"google.golang.org/protobuf/types/known/durationpb"
3636
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
3737

38+
tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
3839
extensions "istio.io/api/extensions/v1alpha1"
3940
meshconfig "istio.io/api/mesh/v1alpha1"
4041
networking "istio.io/api/networking/v1alpha3"
4142
security "istio.io/api/security/v1beta1"
4243
telemetry "istio.io/api/telemetry/v1alpha1"
4344
"istio.io/istio/pilot/pkg/features"
4445
"istio.io/istio/pilot/pkg/model"
46+
istionetworking "istio.io/istio/pilot/pkg/networking"
4547
"istio.io/istio/pilot/pkg/networking/core/listenertest"
4648
"istio.io/istio/pilot/pkg/networking/util"
4749
"istio.io/istio/pilot/pkg/serviceregistry/provider"
@@ -3137,3 +3139,106 @@ func TestListenerTransportSocketConnectTimeoutForSidecar(t *testing.T) {
31373139
})
31383140
}
31393141
}
3142+
3143+
func TestBuildListenerTLSContext(t *testing.T) {
3144+
tests := []struct {
3145+
name string
3146+
serverTLSSettings *networking.ServerTLSSettings
3147+
proxy *model.Proxy
3148+
mesh *meshconfig.MeshConfig
3149+
transportProtocol istionetworking.TransportProtocol
3150+
gatewayTCPServerWithTLS bool
3151+
expectedCertCount int
3152+
expectedValidation bool
3153+
}{
3154+
{
3155+
name: "single certificate with credential name",
3156+
serverTLSSettings: &networking.ServerTLSSettings{
3157+
Mode: networking.ServerTLSSettings_SIMPLE,
3158+
CredentialName: "test-cert",
3159+
},
3160+
proxy: &model.Proxy{
3161+
Metadata: &model.NodeMetadata{},
3162+
},
3163+
mesh: &meshconfig.MeshConfig{},
3164+
transportProtocol: istionetworking.TransportProtocolTCP,
3165+
gatewayTCPServerWithTLS: false,
3166+
expectedCertCount: 1,
3167+
expectedValidation: false,
3168+
},
3169+
{
3170+
name: "multiple certificates with credential names",
3171+
serverTLSSettings: &networking.ServerTLSSettings{
3172+
Mode: networking.ServerTLSSettings_SIMPLE,
3173+
CredentialNames: []string{"rsa-cert", "ecdsa-cert"},
3174+
},
3175+
proxy: &model.Proxy{
3176+
Metadata: &model.NodeMetadata{},
3177+
},
3178+
mesh: &meshconfig.MeshConfig{},
3179+
transportProtocol: istionetworking.TransportProtocolTCP,
3180+
gatewayTCPServerWithTLS: false,
3181+
expectedCertCount: 2,
3182+
expectedValidation: false,
3183+
},
3184+
{
3185+
name: "multiple certificates with mutual TLS",
3186+
serverTLSSettings: &networking.ServerTLSSettings{
3187+
Mode: networking.ServerTLSSettings_MUTUAL,
3188+
CredentialNames: []string{"rsa-cert", "ecdsa-cert"},
3189+
SubjectAltNames: []string{"test.com"},
3190+
},
3191+
proxy: &model.Proxy{
3192+
Metadata: &model.NodeMetadata{},
3193+
},
3194+
mesh: &meshconfig.MeshConfig{},
3195+
transportProtocol: istionetworking.TransportProtocolTCP,
3196+
gatewayTCPServerWithTLS: false,
3197+
expectedCertCount: 2,
3198+
expectedValidation: true,
3199+
},
3200+
{
3201+
name: "prefer credential names over credential name",
3202+
serverTLSSettings: &networking.ServerTLSSettings{
3203+
Mode: networking.ServerTLSSettings_SIMPLE,
3204+
CredentialName: "old-cert",
3205+
CredentialNames: []string{"rsa-cert", "ecdsa-cert"},
3206+
},
3207+
proxy: &model.Proxy{
3208+
Metadata: &model.NodeMetadata{},
3209+
},
3210+
mesh: &meshconfig.MeshConfig{},
3211+
transportProtocol: istionetworking.TransportProtocolTCP,
3212+
gatewayTCPServerWithTLS: false,
3213+
expectedCertCount: 2,
3214+
expectedValidation: false,
3215+
},
3216+
}
3217+
3218+
for _, tt := range tests {
3219+
t.Run(tt.name, func(t *testing.T) {
3220+
ctx := BuildListenerTLSContext(tt.serverTLSSettings, tt.proxy, tt.mesh, tt.transportProtocol, tt.gatewayTCPServerWithTLS)
3221+
3222+
// Check certificate count
3223+
if len(ctx.CommonTlsContext.TlsCertificateSdsSecretConfigs) != tt.expectedCertCount {
3224+
t.Errorf("expected %d certificates, got %d", tt.expectedCertCount, len(ctx.CommonTlsContext.TlsCertificateSdsSecretConfigs))
3225+
}
3226+
3227+
// Check validation context
3228+
if tt.expectedValidation {
3229+
if ctx.CommonTlsContext.ValidationContextType == nil {
3230+
t.Error("expected validation context to be set")
3231+
}
3232+
combinedCtx, ok := ctx.CommonTlsContext.ValidationContextType.(*tls.CommonTlsContext_CombinedValidationContext)
3233+
if !ok {
3234+
t.Error("expected CombinedValidationContext")
3235+
}
3236+
if combinedCtx.CombinedValidationContext == nil {
3237+
t.Error("expected CombinedValidationContext to be set")
3238+
}
3239+
} else if ctx.CommonTlsContext.ValidationContextType != nil {
3240+
t.Error("unexpected validation context")
3241+
}
3242+
})
3243+
}
3244+
}

pilot/pkg/security/model/authentication.go

+51-35
Original file line numberDiff line numberDiff line change
@@ -129,46 +129,52 @@ func AppendURIPrefixToTrustDomain(trustDomainAliases []string) []string {
129129
func ApplyToCommonTLSContext(tlsContext *tls.CommonTlsContext, proxy *model.Proxy,
130130
subjectAltNames []string, crl string, trustDomainAliases []string, validateClient bool,
131131
) {
132-
customFileSDSServer := proxy.Metadata.Raw[security.CredentialFileMetaDataName] == "true"
133-
// These are certs being mounted from within the pod. Rather than reading directly in Envoy,
134-
// which does not support rotation, we will serve them over SDS by reading the files.
135-
// We should check if these certs have values, if yes we should use them or otherwise fall back to defaults.
136-
res := security.SdsCertificateConfig{
137-
CertificatePath: proxy.Metadata.TLSServerCertChain,
138-
PrivateKeyPath: proxy.Metadata.TLSServerKey,
139-
CaCertificatePath: proxy.Metadata.TLSServerRootCert,
140-
}
141-
142-
// TODO: if subjectAltName ends with *, create a prefix match as well.
143-
// TODO: if user explicitly specifies SANs - should we alter his explicit config by adding all spifee aliases?
144-
matchSAN := util.StringToExactMatch(subjectAltNames)
145-
if len(trustDomainAliases) > 0 {
146-
matchSAN = append(matchSAN, util.StringToPrefixMatch(AppendURIPrefixToTrustDomain(trustDomainAliases))...)
132+
if proxy.Metadata.TLSServerCertificates == nil {
133+
// Create a default TLS server certificate
134+
proxy.Metadata.TLSServerCertificates = []*model.TLSServerCertificate{{}}
147135
}
136+
sdsSecretConfigs := make([]*tls.SdsSecretConfig, len(proxy.Metadata.TLSServerCertificates))
137+
for i, cert := range proxy.Metadata.TLSServerCertificates {
138+
customFileSDSServer := proxy.Metadata.Raw[security.CredentialFileMetaDataName] == "true"
139+
// These are certs being mounted from within the pod. Rather than reading directly in Envoy,
140+
// which does not support rotation, we will serve them over SDS by reading the files.
141+
// We should check if these certs have values, if yes we should use them or otherwise fall back to defaults.
142+
res := security.SdsCertificateConfig{
143+
CertificatePath: cert.TLSServerCertChain,
144+
PrivateKeyPath: cert.TLSServerKey,
145+
CaCertificatePath: cert.TLSServerRootCert,
146+
}
148147

149-
// configure server listeners with SDS.
150-
if validateClient {
151-
defaultValidationContext := &tls.CertificateValidationContext{
152-
MatchSubjectAltNames: matchSAN,
148+
// TODO: if subjectAltName ends with *, create a prefix match as well.
149+
// TODO: if user explicitly specifies SANs - should we alter his explicit config by adding all spifee aliases?
150+
matchSAN := util.StringToExactMatch(subjectAltNames)
151+
if len(trustDomainAliases) > 0 {
152+
matchSAN = append(matchSAN, util.StringToPrefixMatch(AppendURIPrefixToTrustDomain(trustDomainAliases))...)
153153
}
154-
if crl != "" {
155-
defaultValidationContext.Crl = &core.DataSource{
156-
Specifier: &core.DataSource_Filename{
157-
Filename: crl,
154+
155+
// configure server listeners with SDS.
156+
if validateClient {
157+
defaultValidationContext := &tls.CertificateValidationContext{
158+
MatchSubjectAltNames: matchSAN,
159+
}
160+
if crl != "" {
161+
defaultValidationContext.Crl = &core.DataSource{
162+
Specifier: &core.DataSource_Filename{
163+
Filename: crl,
164+
},
165+
}
166+
}
167+
tlsContext.ValidationContextType = &tls.CommonTlsContext_CombinedValidationContext{
168+
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
169+
DefaultValidationContext: defaultValidationContext,
170+
ValidationContextSdsSecretConfig: constructSdsSecretConfig(res.GetRootResourceName(), SDSRootResourceName, customFileSDSServer),
158171
},
159172
}
160-
}
161-
tlsContext.ValidationContextType = &tls.CommonTlsContext_CombinedValidationContext{
162-
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
163-
DefaultValidationContext: defaultValidationContext,
164-
ValidationContextSdsSecretConfig: constructSdsSecretConfig(res.GetRootResourceName(), SDSRootResourceName, customFileSDSServer),
165-
},
166-
}
167173

174+
}
175+
sdsSecretConfigs[i] = constructSdsSecretConfig(res.GetResourceName(), SDSDefaultResourceName, customFileSDSServer)
168176
}
169-
tlsContext.TlsCertificateSdsSecretConfigs = []*tls.SdsSecretConfig{
170-
constructSdsSecretConfig(res.GetResourceName(), SDSDefaultResourceName, customFileSDSServer),
171-
}
177+
tlsContext.TlsCertificateSdsSecretConfigs = sdsSecretConfigs
172178
}
173179

174180
// constructSdsSecretConfig allows passing a file name and a fallback.
@@ -217,9 +223,19 @@ func ApplyCredentialSDSToServerCommonTLSContext(tlsContext *tls.CommonTlsContext
217223
tlsOpts *networking.ServerTLSSettings, credentialSocketExist bool,
218224
) {
219225
// create SDS config for gateway/sidecar to fetch key/cert from agent.
220-
tlsContext.TlsCertificateSdsSecretConfigs = []*tls.SdsSecretConfig{
221-
ConstructSdsSecretConfigForCredential(tlsOpts.CredentialName, credentialSocketExist),
226+
if len(tlsOpts.CredentialNames) > 0 {
227+
// Handle multiple certificates for RSA and ECDSA
228+
tlsContext.TlsCertificateSdsSecretConfigs = make([]*tls.SdsSecretConfig, len(tlsOpts.CredentialNames))
229+
for i, name := range tlsOpts.CredentialNames {
230+
tlsContext.TlsCertificateSdsSecretConfigs[i] = ConstructSdsSecretConfigForCredential(name, credentialSocketExist)
231+
}
232+
} else {
233+
// Handle single certificate
234+
tlsContext.TlsCertificateSdsSecretConfigs = []*tls.SdsSecretConfig{
235+
ConstructSdsSecretConfigForCredential(tlsOpts.CredentialName, credentialSocketExist),
236+
}
222237
}
238+
223239
// If tls mode is MUTUAL/OPTIONAL_MUTUAL, create SDS config for gateway/sidecar to fetch certificate validation context
224240
// at gateway agent. Otherwise, use the static certificate validation context config.
225241
if tlsOpts.Mode == networking.ServerTLSSettings_MUTUAL || tlsOpts.Mode == networking.ServerTLSSettings_OPTIONAL_MUTUAL {

0 commit comments

Comments
 (0)