/
policy_applier.go
487 lines (433 loc) · 18.3 KB
/
policy_applier.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1beta1
import (
"fmt"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
envoy_jwt "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/emptypb"
authn_alpha "istio.io/api/authentication/v1alpha1"
authn_filter "istio.io/api/envoy/config/filter/http/authn/v2alpha1"
meshconfig "istio.io/api/mesh/v1alpha1"
"istio.io/api/security/v1beta1"
"istio.io/istio/pilot/pkg/features"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pilot/pkg/networking"
"istio.io/istio/pilot/pkg/security/authn"
authn_utils "istio.io/istio/pilot/pkg/security/authn/utils"
authn_model "istio.io/istio/pilot/pkg/security/model"
"istio.io/istio/pilot/pkg/util/protoconv"
"istio.io/istio/pilot/pkg/xds/filters"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/security"
"istio.io/istio/pkg/jwt"
"istio.io/istio/pkg/log"
"istio.io/istio/pkg/slices"
)
var authnLog = log.RegisterScope("authn", "authn debugging")
// Implementation of authn.PolicyApplier with v1beta1 API.
type v1beta1PolicyApplier struct {
// processedJwtRules is the consolidate JWT rules from all jwtPolicies.
processedJwtRules []*v1beta1.JWTRule
consolidatedPeerPolicy MergedPeerAuthentication
push *model.PushContext
}
// NewPolicyApplier returns new applier for v1beta1 authentication policies.
func NewPolicyApplier(rootNamespace string,
jwtPolicies []*config.Config,
peerPolicies []*config.Config,
push *model.PushContext,
) authn.PolicyApplier {
processedJwtRules := []*v1beta1.JWTRule{}
// TODO(diemtvu) should we need to deduplicate JWT with the same issuer.
// https://github.com/istio/istio/issues/19245
for idx := range jwtPolicies {
spec := jwtPolicies[idx].Spec.(*v1beta1.RequestAuthentication)
processedJwtRules = append(processedJwtRules, spec.JwtRules...)
}
// Sort the jwt rules by the issuer alphabetically to make the later-on generated filter
// config deterministic.
slices.SortFunc(processedJwtRules, func(a, b *v1beta1.JWTRule) bool {
return a.GetIssuer() < b.GetIssuer()
})
return v1beta1PolicyApplier{
processedJwtRules: processedJwtRules,
consolidatedPeerPolicy: ComposePeerAuthentication(rootNamespace, peerPolicies),
push: push,
}
}
func (a v1beta1PolicyApplier) JwtFilter() *hcm.HttpFilter {
if len(a.processedJwtRules) == 0 {
return nil
}
filterConfigProto := convertToEnvoyJwtConfig(a.processedJwtRules, a.push)
if filterConfigProto == nil {
return nil
}
return &hcm.HttpFilter{
Name: authn_model.EnvoyJwtFilterName,
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(filterConfigProto)},
}
}
func defaultAuthnFilter() *authn_filter.FilterConfig {
return &authn_filter.FilterConfig{
Policy: &authn_alpha.Policy{},
// we can always set this field, it's no-op if mTLS is not used.
SkipValidateTrustDomain: true,
}
}
func (a v1beta1PolicyApplier) setAuthnFilterForRequestAuthn(config *authn_filter.FilterConfig) *authn_filter.FilterConfig {
if len(a.processedJwtRules) == 0 {
// (beta) RequestAuthentication is not set for workload, do nothing.
authnLog.Debug("AuthnFilter: RequestAuthentication (beta policy) not found, keep settings with alpha API")
return config
}
if config == nil {
config = defaultAuthnFilter()
}
// This is obsoleted and not needed (payload is extracted from metadata). Reset the field to remove
// any artifacts from alpha applier.
config.JwtOutputPayloadLocations = nil
p := config.Policy
// Reset origins to use with beta API
// nolint: staticcheck
p.Origins = []*authn_alpha.OriginAuthenticationMethod{}
// Always set to true for beta API, as it doesn't doe rejection on missing token.
// nolint: staticcheck
p.OriginIsOptional = true
// Always bind request.auth.principal from JWT origin. In v2 policy, authorization config specifies what principal to
// choose from instead, rather than in authn config.
// nolint: staticcheck
p.PrincipalBinding = authn_alpha.PrincipalBinding_USE_ORIGIN
// nolint: staticcheck
for _, jwt := range a.processedJwtRules {
p.Origins = append(p.Origins, &authn_alpha.OriginAuthenticationMethod{
Jwt: &authn_alpha.Jwt{
// used for getting the filter data, and all other fields are irrelevant.
Issuer: jwt.GetIssuer(),
},
})
}
return config
}
// AuthNFilter returns the Istio authn filter config:
// - If RequestAuthentication is used, it overwrite the settings for request principal validation and extraction based on the new API.
// - If RequestAuthentication is used, principal binding is always set to ORIGIN.
func (a v1beta1PolicyApplier) AuthNFilter(forSidecar bool) *hcm.HttpFilter {
var filterConfigProto *authn_filter.FilterConfig
// Override the config with request authentication, if applicable.
filterConfigProto = a.setAuthnFilterForRequestAuthn(filterConfigProto)
if filterConfigProto == nil {
return nil
}
// disable clear route cache for sidecars because the JWT claim based routing is only supported on gateways.
filterConfigProto.DisableClearRouteCache = forSidecar
// Note: in previous Istio versions, the authn filter also handled PeerAuthentication, to extract principal.
// This has been modified to rely on the TCP filter
return &hcm.HttpFilter{
Name: filters.AuthnFilterName,
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(filterConfigProto)},
}
}
func (a v1beta1PolicyApplier) InboundMTLSSettings(
endpointPort uint32,
node *model.Proxy,
trustDomainAliases []string,
modeOverride model.MutualTLSMode,
) authn.MTLSSettings {
effectiveMTLSMode := modeOverride
if effectiveMTLSMode == model.MTLSUnknown {
effectiveMTLSMode = a.GetMutualTLSModeForPort(endpointPort)
}
authnLog.Debugf("InboundFilterChain: build inbound filter change for %v:%d in %s mode", node.ID, endpointPort, effectiveMTLSMode)
var mc *meshconfig.MeshConfig
if a.push != nil {
mc = a.push.Mesh
}
// Configure TLS version based on meshconfig TLS API.
// This is used to configure TLS version for inbound filter chain of ISTIO MUTUAL cases.
// For MUTUAL and SIMPLE TLS modes specified via ServerTLSSettings in Sidecar or Gateway,
// TLS version is configured in the BuildListenerContext.
minTLSVersion := authn_utils.GetMinTLSVersion(mc.GetMeshMTLS().GetMinProtocolVersion())
return authn.MTLSSettings{
Port: endpointPort,
Mode: effectiveMTLSMode,
TCP: authn_utils.BuildInboundTLS(effectiveMTLSMode, node, networking.ListenerProtocolTCP,
trustDomainAliases, minTLSVersion, mc),
HTTP: authn_utils.BuildInboundTLS(effectiveMTLSMode, node, networking.ListenerProtocolHTTP,
trustDomainAliases, minTLSVersion, mc),
}
}
// convertToEnvoyJwtConfig converts a list of JWT rules into Envoy JWT filter config to enforce it.
// Each rule is expected corresponding to one JWT issuer (provider).
// The behavior of the filter should reject all requests with invalid token. On the other hand,
// if no token provided, the request is allowed.
func convertToEnvoyJwtConfig(jwtRules []*v1beta1.JWTRule, push *model.PushContext) *envoy_jwt.JwtAuthentication {
if len(jwtRules) == 0 {
return nil
}
providers := map[string]*envoy_jwt.JwtProvider{}
// Each element of innerAndList is the requirement for each provider, in the form of
// {provider OR `allow_missing`}
// This list will be ANDed (if have more than one provider) for the final requirement.
innerAndList := []*envoy_jwt.JwtRequirement{}
// This is an (or) list for all providers. This will be OR with the innerAndList above so
// it can pass the requirement in the case that providers share the same location.
outterOrList := []*envoy_jwt.JwtRequirement{}
for i, jwtRule := range jwtRules {
provider := &envoy_jwt.JwtProvider{
Issuer: jwtRule.Issuer,
Audiences: jwtRule.Audiences,
Forward: jwtRule.ForwardOriginalToken,
ForwardPayloadHeader: jwtRule.OutputPayloadToHeader,
PayloadInMetadata: jwtRule.Issuer,
}
for _, claimAndHeader := range jwtRule.OutputClaimToHeaders {
provider.ClaimToHeaders = append(provider.ClaimToHeaders, &envoy_jwt.JwtClaimToHeader{
HeaderName: claimAndHeader.Header,
ClaimName: claimAndHeader.Claim,
})
}
for _, location := range jwtRule.FromHeaders {
provider.FromHeaders = append(provider.FromHeaders, &envoy_jwt.JwtHeader{
Name: location.Name,
ValuePrefix: location.Prefix,
})
}
provider.FromParams = jwtRule.FromParams
authnLog.Debugf("JwksFetchMode is set to: %v", features.JwksFetchMode)
// Use Envoy remote jwks if jwksUri is not empty and JwksFetchMode not Istiod. Parse the jwksUri to get the
// cluster name, generate the jwt filter config using remote Jwks.
// If failed to parse the cluster name, only fallback to let istiod to fetch the jwksUri when
// remoteJwksMode is Hybrid.
if features.JwksFetchMode != jwt.Istiod && jwtRule.JwksUri != "" {
jwksInfo, err := security.ParseJwksURI(jwtRule.JwksUri)
if err != nil {
authnLog.Errorf("Failed to parse jwt rule jwks uri %v", err)
}
_, cluster, err := model.LookupCluster(push, jwksInfo.Hostname.String(), jwksInfo.Port)
authnLog.Debugf("Look up cluster result: %v", cluster)
if err == nil && len(cluster) > 0 {
// This is a case of URI pointing to mesh cluster. Setup Remote Jwks and let Envoy fetch the key.
provider.JwksSourceSpecifier = &envoy_jwt.JwtProvider_RemoteJwks{
RemoteJwks: &envoy_jwt.RemoteJwks{
HttpUri: &core.HttpUri{
Uri: jwtRule.JwksUri,
HttpUpstreamType: &core.HttpUri_Cluster{
Cluster: cluster,
},
Timeout: &durationpb.Duration{Seconds: 5},
},
CacheDuration: &durationpb.Duration{Seconds: 5 * 60},
},
}
} else if features.JwksFetchMode == jwt.Hybrid {
provider.JwksSourceSpecifier = push.JwtKeyResolver.BuildLocalJwks(jwtRule.JwksUri, jwtRule.Issuer, "")
} else {
model.IncLookupClusterFailures("jwks")
// Log error and create remote JWKs with fake cluster
authnLog.Errorf("Failed to look up Envoy cluster %v. "+
"Please create ServiceEntry to register external JWKs server or "+
"set PILOT_JWT_ENABLE_REMOTE_JWKS to hybrid/istiod mode.", err)
provider.JwksSourceSpecifier = &envoy_jwt.JwtProvider_RemoteJwks{
RemoteJwks: &envoy_jwt.RemoteJwks{
HttpUri: &core.HttpUri{
Uri: jwtRule.JwksUri,
HttpUpstreamType: &core.HttpUri_Cluster{
Cluster: model.BuildSubsetKey(model.TrafficDirectionOutbound, "", jwksInfo.Hostname, jwksInfo.Port),
},
Timeout: &durationpb.Duration{Seconds: 5},
},
CacheDuration: &durationpb.Duration{Seconds: 5 * 60},
},
}
}
} else {
// Use inline jwks as existing flow, either jwtRule.jwks is empty or let istiod to fetch the jwtRule.jwksUri
provider.JwksSourceSpecifier = push.JwtKeyResolver.BuildLocalJwks(jwtRule.JwksUri, jwtRule.Issuer, jwtRule.Jwks)
}
name := fmt.Sprintf("origins-%d", i)
providers[name] = provider
innerAndList = append(innerAndList, &envoy_jwt.JwtRequirement{
RequiresType: &envoy_jwt.JwtRequirement_RequiresAny{
RequiresAny: &envoy_jwt.JwtRequirementOrList{
Requirements: []*envoy_jwt.JwtRequirement{
{
RequiresType: &envoy_jwt.JwtRequirement_ProviderName{
ProviderName: name,
},
},
{
RequiresType: &envoy_jwt.JwtRequirement_AllowMissing{
AllowMissing: &emptypb.Empty{},
},
},
},
},
},
})
outterOrList = append(outterOrList, &envoy_jwt.JwtRequirement{
RequiresType: &envoy_jwt.JwtRequirement_ProviderName{
ProviderName: name,
},
})
}
// If there is only one provider, simply use an OR of {provider, `allow_missing`}.
if len(innerAndList) == 1 {
return &envoy_jwt.JwtAuthentication{
Rules: []*envoy_jwt.RequirementRule{
{
Match: &route.RouteMatch{
PathSpecifier: &route.RouteMatch_Prefix{
Prefix: "/",
},
},
RequirementType: &envoy_jwt.RequirementRule_Requires{
Requires: innerAndList[0],
},
},
},
Providers: providers,
BypassCorsPreflight: true,
}
}
// If there are more than one provider, filter should OR of
// {P1, P2 .., AND of {OR{P1, allow_missing}, OR{P2, allow_missing} ...}}
// where the innerAnd enforce a token, if provided, must be valid, and the
// outer OR aids the case where providers share the same location (as
// it will always fail with the innerAND).
outterOrList = append(outterOrList, &envoy_jwt.JwtRequirement{
RequiresType: &envoy_jwt.JwtRequirement_RequiresAll{
RequiresAll: &envoy_jwt.JwtRequirementAndList{
Requirements: innerAndList,
},
},
})
return &envoy_jwt.JwtAuthentication{
Rules: []*envoy_jwt.RequirementRule{
{
Match: &route.RouteMatch{
PathSpecifier: &route.RouteMatch_Prefix{
Prefix: "/",
},
},
RequirementType: &envoy_jwt.RequirementRule_Requires{
Requires: &envoy_jwt.JwtRequirement{
RequiresType: &envoy_jwt.JwtRequirement_RequiresAny{
RequiresAny: &envoy_jwt.JwtRequirementOrList{
Requirements: outterOrList,
},
},
},
},
},
},
Providers: providers,
BypassCorsPreflight: true,
}
}
func (a v1beta1PolicyApplier) PortLevelSetting() map[uint32]model.MutualTLSMode {
return a.consolidatedPeerPolicy.PerPort
}
func (a v1beta1PolicyApplier) GetMutualTLSModeForPort(endpointPort uint32) model.MutualTLSMode {
if portMtls, ok := a.consolidatedPeerPolicy.PerPort[endpointPort]; ok {
return portMtls
}
return a.consolidatedPeerPolicy.Mode
}
type MergedPeerAuthentication struct {
// Mode is the overall mode of policy. May be overridden by PerPort
Mode model.MutualTLSMode
// PerPort is the per-port policy
PerPort map[uint32]model.MutualTLSMode
}
// ComposePeerAuthentication returns the effective PeerAuthentication given the list of applicable
// configs. This list should contains at most 1 mesh-level and 1 namespace-level configs.
// Workload-level configs should not be in root namespace (this should be guaranteed by the caller,
// though they will be safely ignored in this function). If the input config list is empty, returns
// a default policy set to a PERMISSIVE.
// If there is at least one applicable config, returns should not be nil, and is a combined policy
// based on following rules:
// - It should have the setting from the most narrow scope (i.e workload-level is preferred over
// namespace-level, which is preferred over mesh-level).
// - When there are more than one policy in the same scope (i.e workload-level), the oldest one win.
// - UNSET will be replaced with the setting from the parent. I.e UNSET port-level config will be
// replaced with config from workload-level, UNSET in workload-level config will be replaced with
// one in namespace-level and so on.
func ComposePeerAuthentication(rootNamespace string, configs []*config.Config) MergedPeerAuthentication {
var meshCfg, namespaceCfg, workloadCfg *config.Config
// Initial outputPolicy is set to a PERMISSIVE.
outputPolicy := MergedPeerAuthentication{
Mode: model.MTLSPermissive,
}
for _, cfg := range configs {
spec := cfg.Spec.(*v1beta1.PeerAuthentication)
if spec.Selector == nil || len(spec.Selector.MatchLabels) == 0 {
// Namespace-level or mesh-level policy
if cfg.Namespace == rootNamespace {
if meshCfg == nil || cfg.CreationTimestamp.Before(meshCfg.CreationTimestamp) {
authnLog.Debugf("Switch selected mesh policy to %s.%s (%v)", cfg.Name, cfg.Namespace, cfg.CreationTimestamp)
meshCfg = cfg
}
} else {
if namespaceCfg == nil || cfg.CreationTimestamp.Before(namespaceCfg.CreationTimestamp) {
authnLog.Debugf("Switch selected namespace policy to %s.%s (%v)", cfg.Name, cfg.Namespace, cfg.CreationTimestamp)
namespaceCfg = cfg
}
}
} else if cfg.Namespace != rootNamespace {
// Workload-level policy, aka the one with selector and not in root namespace.
if workloadCfg == nil || cfg.CreationTimestamp.Before(workloadCfg.CreationTimestamp) {
authnLog.Debugf("Switch selected workload policy to %s.%s (%v)", cfg.Name, cfg.Namespace, cfg.CreationTimestamp)
workloadCfg = cfg
}
}
}
// Process in mesh, namespace, workload order to resolve inheritance (UNSET)
if meshCfg != nil && !isMtlsModeUnset(meshCfg.Spec.(*v1beta1.PeerAuthentication).Mtls) {
// If mesh policy is defined, update parent policy to mesh policy.
outputPolicy.Mode = model.ConvertToMutualTLSMode(meshCfg.Spec.(*v1beta1.PeerAuthentication).Mtls.Mode)
}
if namespaceCfg != nil && !isMtlsModeUnset(namespaceCfg.Spec.(*v1beta1.PeerAuthentication).Mtls) {
// If namespace policy is defined, update output policy to namespace policy. This means namespace
// policy overwrite mesh policy.
outputPolicy.Mode = model.ConvertToMutualTLSMode(namespaceCfg.Spec.(*v1beta1.PeerAuthentication).Mtls.Mode)
}
var workloadPolicy *v1beta1.PeerAuthentication
if workloadCfg != nil {
workloadPolicy = workloadCfg.Spec.(*v1beta1.PeerAuthentication)
}
if workloadPolicy != nil && !isMtlsModeUnset(workloadPolicy.Mtls) {
// If workload policy is defined, update parent policy to workload policy.
outputPolicy.Mode = model.ConvertToMutualTLSMode(workloadPolicy.Mtls.Mode)
}
if workloadPolicy != nil && workloadPolicy.PortLevelMtls != nil {
outputPolicy.PerPort = make(map[uint32]model.MutualTLSMode, len(workloadPolicy.PortLevelMtls))
for port, mtls := range workloadPolicy.PortLevelMtls {
if isMtlsModeUnset(mtls) {
// Inherit from workload level.
outputPolicy.PerPort[port] = outputPolicy.Mode
} else {
outputPolicy.PerPort[port] = model.ConvertToMutualTLSMode(mtls.Mode)
}
}
}
return outputPolicy
}
func isMtlsModeUnset(mtls *v1beta1.PeerAuthentication_MutualTLS) bool {
return mtls == nil || mtls.Mode == v1beta1.PeerAuthentication_MutualTLS_UNSET
}