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  }