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