1// Copyright 2018 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package model
16
17import (
18	"sync"
19
20	auth "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
21	core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
22	envoy_config_grpc_credential_v2alpha "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v2alpha"
23	"github.com/golang/protobuf/ptypes"
24	"github.com/golang/protobuf/ptypes/any"
25
26	networking "istio.io/api/networking/v1alpha3"
27
28	"istio.io/istio/pilot/pkg/features"
29	"istio.io/istio/pilot/pkg/model"
30	"istio.io/istio/pilot/pkg/networking/util"
31	"istio.io/istio/pkg/config/constants"
32)
33
34const (
35	// SDSStatPrefix is the human readable prefix to use when emitting statistics for the SDS service.
36	SDSStatPrefix = "sdsstat"
37
38	// SDSClusterName is the name of the cluster for SDS connections
39	SDSClusterName = "sds-grpc"
40
41	// SDSDefaultResourceName is the default name in sdsconfig, used for fetching normal key/cert.
42	SDSDefaultResourceName = "default"
43
44	// SDSRootResourceName is the sdsconfig name for root CA, used for fetching root cert.
45	SDSRootResourceName = "ROOTCA"
46
47	// K8sSAJwtFileName is the token volume mount file name for k8s jwt token.
48	K8sSAJwtFileName = "/var/run/secrets/kubernetes.io/serviceaccount/token"
49
50	// K8sSATrustworthyJwtFileName is the token volume mount file name for k8s trustworthy jwt token.
51	K8sSATrustworthyJwtFileName = "/var/run/secrets/tokens/istio-token"
52
53	// FileBasedMetadataPlugName is File Based Metadata credentials plugin name.
54	FileBasedMetadataPlugName = "envoy.grpc_credentials.file_based_metadata"
55
56	// K8sSAJwtTokenHeaderKey is the request header key for k8s jwt token.
57	// Binary header name must has suffix "-bin", according to https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md.
58	K8sSAJwtTokenHeaderKey = "istio_sds_credentials_header-bin"
59
60	// IngressGatewaySdsUdsPath is the UDS path for ingress gateway to get credentials via SDS.
61	IngressGatewaySdsUdsPath = "unix:/var/run/ingress_gateway/sds"
62
63	// SdsCaSuffix is the suffix of the sds resource name for root CA.
64	SdsCaSuffix = "-cacert"
65
66	// IstioJwtFilterName is the name for the Istio Jwt filter. This should be the same
67	// as the name defined in
68	// https://github.com/istio/proxy/blob/master/src/envoy/http/jwt_auth/http_filter_factory.cc#L50
69	IstioJwtFilterName = "jwt-auth"
70
71	// EnvoyJwtFilterName is the name of the Envoy JWT filter. This should be the same as the name defined
72	// in https://github.com/envoyproxy/envoy/blob/v1.9.1/source/extensions/filters/http/well_known_names.h#L48
73	EnvoyJwtFilterName = "envoy.filters.http.jwt_authn"
74
75	// AuthnFilterName is the name for the Istio AuthN filter. This should be the same
76	// as the name defined in
77	// https://github.com/istio/proxy/blob/master/src/envoy/http/authn/http_filter_factory.cc#L30
78	AuthnFilterName = "istio_authn"
79)
80
81// ConstructSdsSecretConfigWithCustomUds constructs SDS secret configuration for ingress gateway.
82func ConstructSdsSecretConfigWithCustomUds(name, sdsUdsPath string) *auth.SdsSecretConfig {
83	if name == "" || sdsUdsPath == "" {
84		return nil
85	}
86
87	gRPCConfig := &core.GrpcService_GoogleGrpc{
88		TargetUri:  sdsUdsPath,
89		StatPrefix: SDSStatPrefix,
90	}
91
92	return &auth.SdsSecretConfig{
93		Name: name,
94		SdsConfig: &core.ConfigSource{
95			ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
96				ApiConfigSource: &core.ApiConfigSource{
97					ApiType: core.ApiConfigSource_GRPC,
98					GrpcServices: []*core.GrpcService{
99						{
100							TargetSpecifier: &core.GrpcService_GoogleGrpc_{
101								GoogleGrpc: gRPCConfig,
102							},
103						},
104					},
105				},
106			},
107			InitialFetchTimeout: features.InitialFetchTimeout,
108		},
109	}
110}
111
112// ConstructSdsSecretConfig constructs SDS Secret Configuration for workload proxy.
113func ConstructSdsSecretConfig(name, sdsUdsPath string) *auth.SdsSecretConfig {
114	if name == "" || sdsUdsPath == "" {
115		return nil
116	}
117
118	return &auth.SdsSecretConfig{
119		Name: name,
120		SdsConfig: &core.ConfigSource{
121			ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
122				ApiConfigSource: &core.ApiConfigSource{
123					ApiType: core.ApiConfigSource_GRPC,
124					GrpcServices: []*core.GrpcService{
125						{
126							TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
127								EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: SDSClusterName},
128							},
129						},
130					},
131				},
132			},
133			InitialFetchTimeout: features.InitialFetchTimeout,
134		},
135	}
136}
137
138// ConstructValidationContext constructs ValidationContext in CommonTLSContext.
139func ConstructValidationContext(rootCAFilePath string, subjectAltNames []string) *auth.CommonTlsContext_ValidationContext {
140	ret := &auth.CommonTlsContext_ValidationContext{
141		ValidationContext: &auth.CertificateValidationContext{
142			TrustedCa: &core.DataSource{
143				Specifier: &core.DataSource_Filename{
144					Filename: rootCAFilePath,
145				},
146			},
147		},
148	}
149
150	if len(subjectAltNames) > 0 {
151		ret.ValidationContext.MatchSubjectAltNames = util.StringToExactMatch(subjectAltNames)
152	}
153
154	return ret
155}
156
157// ApplyToCommonTLSContext completes the commonTlsContext for `ISTIO_MUTUAL` TLS mode
158func ApplyToCommonTLSContext(tlsContext *auth.CommonTlsContext, metadata *model.NodeMetadata, sdsPath string, subjectAltNames []string) {
159	// configure TLS with SDS
160	if metadata.SdsEnabled && sdsPath != "" {
161		// These are certs being mounted from within the pod. Rather than reading directly in Envoy,
162		// which does not support rotation, we will serve them over SDS by reading the files.
163		// We should check if these certs have values, if yes we should use them or otherwise fall back to defaults.
164		res := model.SdsCertificateConfig{
165			CertificatePath:   metadata.TLSServerCertChain,
166			PrivateKeyPath:    metadata.TLSServerKey,
167			CaCertificatePath: metadata.TLSServerRootCert,
168		}
169
170		// configure server listeners with SDS.
171		tlsContext.ValidationContextType = &auth.CommonTlsContext_CombinedValidationContext{
172			CombinedValidationContext: &auth.CommonTlsContext_CombinedCertificateValidationContext{
173				DefaultValidationContext: &auth.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch(subjectAltNames)},
174				ValidationContextSdsSecretConfig: ConstructSdsSecretConfig(
175					model.GetOrDefault(res.GetRootResourceName(), SDSRootResourceName), sdsPath),
176			},
177		}
178		tlsContext.TlsCertificateSdsSecretConfigs = []*auth.SdsSecretConfig{
179			ConstructSdsSecretConfig(model.GetOrDefault(res.GetResourceName(), SDSDefaultResourceName), sdsPath),
180		}
181	} else {
182		// TODO(ramaraochavali): Clean this codepath later as we default to SDS.
183		// SDS disabled, fall back on using mounted certificates
184		base := metadata.SdsBase + constants.AuthCertsPath
185		tlsServerRootCert := model.GetOrDefault(metadata.TLSServerRootCert, base+constants.RootCertFilename)
186
187		tlsContext.ValidationContextType = ConstructValidationContext(tlsServerRootCert, subjectAltNames)
188
189		tlsServerCertChain := model.GetOrDefault(metadata.TLSServerCertChain, base+constants.CertChainFilename)
190		tlsServerKey := model.GetOrDefault(metadata.TLSServerKey, base+constants.KeyFilename)
191
192		tlsContext.TlsCertificates = []*auth.TlsCertificate{
193			{
194				CertificateChain: &core.DataSource{
195					Specifier: &core.DataSource_Filename{
196						Filename: tlsServerCertChain,
197					},
198				},
199				PrivateKey: &core.DataSource{
200					Specifier: &core.DataSource_Filename{
201						Filename: tlsServerKey,
202					},
203				},
204			},
205		}
206	}
207}
208
209// ApplyCustomSDSToCommonTLSContext applies the customized sds to CommonTlsContext
210// Used for building both gateway/sidecar TLS context
211func ApplyCustomSDSToCommonTLSContext(tlsContext *auth.CommonTlsContext, tlsOpts *networking.ServerTLSSettings, sdsUdsPath string) {
212	// create SDS config for gateway/sidecar to fetch key/cert from agent.
213	tlsContext.TlsCertificateSdsSecretConfigs = []*auth.SdsSecretConfig{
214		ConstructSdsSecretConfigWithCustomUds(tlsOpts.CredentialName, sdsUdsPath),
215	}
216	// If tls mode is MUTUAL, create SDS config for gateway/sidecar to fetch certificate validation context
217	// at gateway agent. Otherwise, use the static certificate validation context config.
218	if tlsOpts.Mode == networking.ServerTLSSettings_MUTUAL {
219		defaultValidationContext := &auth.CertificateValidationContext{
220			MatchSubjectAltNames:  util.StringToExactMatch(tlsOpts.SubjectAltNames),
221			VerifyCertificateSpki: tlsOpts.VerifyCertificateSpki,
222			VerifyCertificateHash: tlsOpts.VerifyCertificateHash,
223		}
224		tlsContext.ValidationContextType = &auth.CommonTlsContext_CombinedValidationContext{
225			CombinedValidationContext: &auth.CommonTlsContext_CombinedCertificateValidationContext{
226				DefaultValidationContext: defaultValidationContext,
227				ValidationContextSdsSecretConfig: ConstructSdsSecretConfigWithCustomUds(
228					tlsOpts.CredentialName+SdsCaSuffix, sdsUdsPath),
229			},
230		}
231	} else if len(tlsOpts.SubjectAltNames) > 0 {
232		tlsContext.ValidationContextType = &auth.CommonTlsContext_ValidationContext{
233			ValidationContext: &auth.CertificateValidationContext{
234				MatchSubjectAltNames: util.StringToExactMatch(tlsOpts.SubjectAltNames),
235			},
236		}
237	}
238}
239
240// ConstructgRPCCallCredentials is used to construct SDS config which is only available from 1.1
241func ConstructgRPCCallCredentials(tokenFileName, headerKey string) []*core.GrpcService_GoogleGrpc_CallCredentials {
242	// If k8s sa jwt token file exists, envoy only handles plugin credentials.
243	config := &envoy_config_grpc_credential_v2alpha.FileBasedMetadataConfig{
244		SecretData: &core.DataSource{
245			Specifier: &core.DataSource_Filename{
246				Filename: tokenFileName,
247			},
248		},
249		HeaderKey: headerKey,
250	}
251
252	any := findOrMarshalFileBasedMetadataConfig(tokenFileName, headerKey, config)
253
254	return []*core.GrpcService_GoogleGrpc_CallCredentials{
255		{
256			CredentialSpecifier: &core.GrpcService_GoogleGrpc_CallCredentials_FromPlugin{
257				FromPlugin: &core.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin{
258					Name: FileBasedMetadataPlugName,
259					ConfigType: &core.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin_TypedConfig{
260						TypedConfig: any},
261				},
262			},
263		},
264	}
265}
266
267type fbMetadataAnyKey struct {
268	tokenFileName string
269	headerKey     string
270}
271
272var fileBasedMetadataConfigAnyMap sync.Map
273
274// findOrMarshalFileBasedMetadataConfig searches google.protobuf.Any in fileBasedMetadataConfigAnyMap
275// by tokenFileName and headerKey, and returns google.protobuf.Any proto if found. If not found,
276// it takes the fbMetadata and marshals it into google.protobuf.Any, and stores this new
277// google.protobuf.Any into fileBasedMetadataConfigAnyMap.
278// FileBasedMetadataConfig only supports non-deterministic marshaling. As each SDS config contains
279// marshaled FileBasedMetadataConfig, the SDS config would differ if marshaling FileBasedMetadataConfig
280// returns different result. Once SDS config differs, Envoy will create multiple SDS clients to fetch
281// same SDS resource. To solve this problem, we use findOrMarshalFileBasedMetadataConfig so that
282// FileBasedMetadataConfig is marshaled once, and is reused in all SDS configs.
283func findOrMarshalFileBasedMetadataConfig(tokenFileName, headerKey string, fbMetadata *envoy_config_grpc_credential_v2alpha.FileBasedMetadataConfig) *any.Any {
284	key := fbMetadataAnyKey{
285		tokenFileName: tokenFileName,
286		headerKey:     headerKey,
287	}
288	if v, found := fileBasedMetadataConfigAnyMap.Load(key); found {
289		marshalAny := v.(any.Any)
290		return &marshalAny
291	}
292	any, _ := ptypes.MarshalAny(fbMetadata)
293	fileBasedMetadataConfigAnyMap.Store(key, *any)
294	return any
295}
296