istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/security/authz/builder/builder.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 builder
    16  
    17  import (
    18  	"fmt"
    19  	"strconv"
    20  
    21  	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    22  	rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    23  	rbachttp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
    24  	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    25  	rbactcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
    26  	"github.com/hashicorp/go-multierror"
    27  
    28  	"istio.io/api/annotation"
    29  	"istio.io/istio/pilot/pkg/model"
    30  	authzmodel "istio.io/istio/pilot/pkg/security/authz/model"
    31  	"istio.io/istio/pilot/pkg/security/trustdomain"
    32  	"istio.io/istio/pilot/pkg/util/protoconv"
    33  	"istio.io/istio/pkg/maps"
    34  	"istio.io/istio/pkg/wellknown"
    35  )
    36  
    37  var rbacPolicyMatchNever = &rbacpb.Policy{
    38  	Permissions: []*rbacpb.Permission{{Rule: &rbacpb.Permission_NotRule{
    39  		NotRule: &rbacpb.Permission{Rule: &rbacpb.Permission_Any{Any: true}},
    40  	}}},
    41  	Principals: []*rbacpb.Principal{{Identifier: &rbacpb.Principal_NotId{
    42  		NotId: &rbacpb.Principal{Identifier: &rbacpb.Principal_Any{Any: true}},
    43  	}}},
    44  }
    45  
    46  // General setting to control behavior
    47  type Option struct {
    48  	IsCustomBuilder bool
    49  	UseFilterState  bool
    50  	UseExtendedJwt  bool
    51  }
    52  
    53  // Builder builds Istio authorization policy to Envoy filters.
    54  type Builder struct {
    55  	trustDomainBundle trustdomain.Bundle
    56  	option            Option
    57  
    58  	// populated when building for CUSTOM action.
    59  	customPolicies []model.AuthorizationPolicy
    60  	extensions     map[string]*builtExtAuthz
    61  
    62  	// populated when building for ALLOW/DENY/AUDIT action.
    63  	denyPolicies  []model.AuthorizationPolicy
    64  	allowPolicies []model.AuthorizationPolicy
    65  	auditPolicies []model.AuthorizationPolicy
    66  
    67  	// logger emits logs about policies
    68  	logger *AuthzLogger
    69  }
    70  
    71  // New returns a new builder for the given workload with the authorization policy.
    72  // Returns nil if none of the authorization policies are enabled for the workload.
    73  func New(trustDomainBundle trustdomain.Bundle, push *model.PushContext, policies model.AuthorizationPoliciesResult, option Option) *Builder {
    74  	if option.IsCustomBuilder {
    75  		if len(policies.Custom) == 0 {
    76  			return nil
    77  		}
    78  		return &Builder{
    79  			customPolicies:    policies.Custom,
    80  			extensions:        processExtensionProvider(push),
    81  			trustDomainBundle: trustDomainBundle,
    82  			option:            option,
    83  		}
    84  	}
    85  
    86  	if len(policies.Deny) == 0 && len(policies.Allow) == 0 && len(policies.Audit) == 0 {
    87  		return nil
    88  	}
    89  	return &Builder{
    90  		denyPolicies:      policies.Deny,
    91  		allowPolicies:     policies.Allow,
    92  		auditPolicies:     policies.Audit,
    93  		trustDomainBundle: trustDomainBundle,
    94  		option:            option,
    95  	}
    96  }
    97  
    98  // BuildHTTP returns the HTTP filters built from the authorization policy.
    99  func (b Builder) BuildHTTP() []*hcm.HttpFilter {
   100  	b.logger = &AuthzLogger{}
   101  	defer b.logger.Report()
   102  	if b.option.IsCustomBuilder {
   103  		// Use the DENY action so that a HTTP rule is properly handled when generating for TCP filter chain.
   104  		if configs := b.build(b.customPolicies, rbacpb.RBAC_DENY, false); configs != nil {
   105  			b.logger.AppendDebugf("built %d HTTP filters for CUSTOM action", len(configs.http))
   106  			return configs.http
   107  		}
   108  		return nil
   109  	}
   110  
   111  	var filters []*hcm.HttpFilter
   112  	if configs := b.build(b.auditPolicies, rbacpb.RBAC_LOG, false); configs != nil {
   113  		b.logger.AppendDebugf("built %d HTTP filters for AUDIT action", len(configs.http))
   114  		filters = append(filters, configs.http...)
   115  	}
   116  	if configs := b.build(b.denyPolicies, rbacpb.RBAC_DENY, false); configs != nil {
   117  		b.logger.AppendDebugf("built %d HTTP filters for DENY action", len(configs.http))
   118  		filters = append(filters, configs.http...)
   119  	}
   120  	if configs := b.build(b.allowPolicies, rbacpb.RBAC_ALLOW, false); configs != nil {
   121  		b.logger.AppendDebugf("built %d HTTP filters for ALLOW action", len(configs.http))
   122  		filters = append(filters, configs.http...)
   123  	}
   124  	return filters
   125  }
   126  
   127  // BuildTCP returns the TCP filters built from the authorization policy.
   128  func (b Builder) BuildTCP() []*listener.Filter {
   129  	b.logger = &AuthzLogger{}
   130  	defer b.logger.Report()
   131  	if b.option.IsCustomBuilder {
   132  		if configs := b.build(b.customPolicies, rbacpb.RBAC_DENY, true); configs != nil {
   133  			b.logger.AppendDebugf("built %d TCP filters for CUSTOM action", len(configs.tcp))
   134  			return configs.tcp
   135  		}
   136  		return nil
   137  	}
   138  
   139  	var filters []*listener.Filter
   140  	if configs := b.build(b.auditPolicies, rbacpb.RBAC_LOG, true); configs != nil {
   141  		b.logger.AppendDebugf("built %d TCP filters for AUDIT action", len(configs.tcp))
   142  		filters = append(filters, configs.tcp...)
   143  	}
   144  	if configs := b.build(b.denyPolicies, rbacpb.RBAC_DENY, true); configs != nil {
   145  		b.logger.AppendDebugf("built %d TCP filters for DENY action", len(configs.tcp))
   146  		filters = append(filters, configs.tcp...)
   147  	}
   148  	if configs := b.build(b.allowPolicies, rbacpb.RBAC_ALLOW, true); configs != nil {
   149  		b.logger.AppendDebugf("built %d TCP filters for ALLOW action", len(configs.tcp))
   150  		filters = append(filters, configs.tcp...)
   151  	}
   152  	return filters
   153  }
   154  
   155  type builtConfigs struct {
   156  	http []*hcm.HttpFilter
   157  	tcp  []*listener.Filter
   158  }
   159  
   160  func (b Builder) isDryRun(policy model.AuthorizationPolicy) bool {
   161  	dryRun := false
   162  	if val, ok := policy.Annotations[annotation.IoIstioDryRun.Name]; ok {
   163  		var err error
   164  		dryRun, err = strconv.ParseBool(val)
   165  		if err != nil {
   166  			b.logger.AppendError(fmt.Errorf("failed to parse the value of %s: %v", annotation.IoIstioDryRun.Name, err))
   167  		}
   168  	}
   169  	return dryRun
   170  }
   171  
   172  func shadowRuleStatPrefix(rule *rbacpb.RBAC) string {
   173  	switch rule.GetAction() {
   174  	case rbacpb.RBAC_ALLOW:
   175  		return authzmodel.RBACShadowRulesAllowStatPrefix
   176  	case rbacpb.RBAC_DENY:
   177  		return authzmodel.RBACShadowRulesDenyStatPrefix
   178  	default:
   179  		return ""
   180  	}
   181  }
   182  
   183  func (b Builder) build(policies []model.AuthorizationPolicy, action rbacpb.RBAC_Action, forTCP bool) *builtConfigs {
   184  	if len(policies) == 0 {
   185  		return nil
   186  	}
   187  
   188  	enforceRules := &rbacpb.RBAC{
   189  		Action:   action,
   190  		Policies: map[string]*rbacpb.Policy{},
   191  	}
   192  	shadowRules := &rbacpb.RBAC{
   193  		Action:   action,
   194  		Policies: map[string]*rbacpb.Policy{},
   195  	}
   196  
   197  	var providers []string
   198  	filterType := "HTTP"
   199  	if forTCP {
   200  		filterType = "TCP"
   201  	}
   202  	hasEnforcePolicy, hasDryRunPolicy := false, false
   203  	for _, policy := range policies {
   204  		var currentRule *rbacpb.RBAC
   205  		if b.isDryRun(policy) {
   206  			currentRule = shadowRules
   207  			hasDryRunPolicy = true
   208  		} else {
   209  			currentRule = enforceRules
   210  			hasEnforcePolicy = true
   211  		}
   212  		if b.option.IsCustomBuilder {
   213  			providers = append(providers, policy.Spec.GetProvider().GetName())
   214  		}
   215  		for i, rule := range policy.Spec.Rules {
   216  			// The name will later be used by ext_authz filter to get the evaluation result from dynamic metadata.
   217  			name := policyName(policy.Namespace, policy.Name, i, b.option)
   218  			if rule == nil {
   219  				b.logger.AppendError(fmt.Errorf("skipped nil rule %s", name))
   220  				continue
   221  			}
   222  			m, err := authzmodel.New(rule, b.option.UseExtendedJwt)
   223  			if err != nil {
   224  				b.logger.AppendError(multierror.Prefix(err, fmt.Sprintf("skipped invalid rule %s:", name)))
   225  				continue
   226  			}
   227  			m.MigrateTrustDomain(b.trustDomainBundle)
   228  			if len(b.trustDomainBundle.TrustDomains) > 1 {
   229  				b.logger.AppendDebugf("patched source principal with trust domain aliases %v", b.trustDomainBundle.TrustDomains)
   230  			}
   231  			generated, err := m.Generate(forTCP, !b.option.UseFilterState, action)
   232  			if err != nil {
   233  				b.logger.AppendDebugf("skipped rule %s on TCP filter chain: %v", name, err)
   234  				continue
   235  			}
   236  			if generated != nil {
   237  				currentRule.Policies[name] = generated
   238  				b.logger.AppendDebugf("generated config from rule %s on %s filter chain successfully", name, filterType)
   239  			}
   240  		}
   241  		if len(policy.Spec.Rules) == 0 {
   242  			// Generate an explicit policy that never matches.
   243  			name := policyName(policy.Namespace, policy.Name, 0, b.option)
   244  			b.logger.AppendDebugf("generated config from policy %s on %s filter chain successfully", name, filterType)
   245  			currentRule.Policies[name] = rbacPolicyMatchNever
   246  		}
   247  	}
   248  
   249  	if !hasEnforcePolicy {
   250  		enforceRules = nil
   251  	}
   252  	if !hasDryRunPolicy {
   253  		shadowRules = nil
   254  	}
   255  	if forTCP {
   256  		return &builtConfigs{tcp: b.buildTCP(enforceRules, shadowRules, providers)}
   257  	}
   258  	return &builtConfigs{http: b.buildHTTP(enforceRules, shadowRules, providers)}
   259  }
   260  
   261  func (b Builder) buildHTTP(rules *rbacpb.RBAC, shadowRules *rbacpb.RBAC, providers []string) []*hcm.HttpFilter {
   262  	if !b.option.IsCustomBuilder {
   263  		rbac := &rbachttp.RBAC{
   264  			Rules:                 rules,
   265  			ShadowRules:           shadowRules,
   266  			ShadowRulesStatPrefix: shadowRuleStatPrefix(shadowRules),
   267  		}
   268  		return []*hcm.HttpFilter{
   269  			{
   270  				Name:       wellknown.HTTPRoleBasedAccessControl,
   271  				ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
   272  			},
   273  		}
   274  	}
   275  
   276  	extauthz, err := getExtAuthz(b.extensions, providers)
   277  	if err != nil {
   278  		b.logger.AppendError(multierror.Prefix(err, "failed to process CUSTOM action, will generate deny configs for the specified rules:"))
   279  		rbac := &rbachttp.RBAC{Rules: getBadCustomDenyRules(rules)}
   280  		return []*hcm.HttpFilter{
   281  			{
   282  				Name:       wellknown.HTTPRoleBasedAccessControl,
   283  				ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
   284  			},
   285  		}
   286  	}
   287  	// Add the RBAC filter in shadow mode so that it only evaluates the matching rules for CUSTOM action but not enforce it.
   288  	// The evaluation result is stored in the dynamic metadata keyed by the policy name. And then the ext_authz filter
   289  	// can utilize these metadata to trigger the enforcement conditionally.
   290  	// See https://docs.google.com/document/d/1V4mCQCw7mlGp0zSQQXYoBdbKMDnkPOjeyUb85U07iSI/edit#bookmark=kix.jdq8u0an2r6s
   291  	// for more details.
   292  	rbac := &rbachttp.RBAC{
   293  		ShadowRules:           rules,
   294  		ShadowRulesStatPrefix: authzmodel.RBACExtAuthzShadowRulesStatPrefix,
   295  	}
   296  	return []*hcm.HttpFilter{
   297  		{
   298  			Name:       wellknown.HTTPRoleBasedAccessControl,
   299  			ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
   300  		},
   301  		{
   302  			Name:       wellknown.HTTPExternalAuthorization,
   303  			ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(extauthz.http)},
   304  		},
   305  	}
   306  }
   307  
   308  func (b Builder) buildTCP(rules *rbacpb.RBAC, shadowRules *rbacpb.RBAC, providers []string) []*listener.Filter {
   309  	if !b.option.IsCustomBuilder {
   310  		rbac := &rbactcp.RBAC{
   311  			Rules:                 rules,
   312  			StatPrefix:            authzmodel.RBACTCPFilterStatPrefix,
   313  			ShadowRules:           shadowRules,
   314  			ShadowRulesStatPrefix: shadowRuleStatPrefix(shadowRules),
   315  		}
   316  		return []*listener.Filter{
   317  			{
   318  				Name:       wellknown.RoleBasedAccessControl,
   319  				ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
   320  			},
   321  		}
   322  	}
   323  
   324  	extauthz, err := getExtAuthz(b.extensions, providers)
   325  	if err != nil {
   326  		b.logger.AppendError(multierror.Prefix(err, "failed to process CUSTOM action, will generate deny configs for the specified rules:"))
   327  		rbac := &rbactcp.RBAC{
   328  			Rules:      getBadCustomDenyRules(rules),
   329  			StatPrefix: authzmodel.RBACTCPFilterStatPrefix,
   330  		}
   331  		return []*listener.Filter{
   332  			{
   333  				Name:       wellknown.RoleBasedAccessControl,
   334  				ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
   335  			},
   336  		}
   337  	} else if extauthz.tcp == nil {
   338  		b.logger.AppendDebugf("ignored CUSTOM action with HTTP provider on TCP filter chain")
   339  		return nil
   340  	}
   341  
   342  	rbac := &rbactcp.RBAC{
   343  		ShadowRules:           rules,
   344  		StatPrefix:            authzmodel.RBACTCPFilterStatPrefix,
   345  		ShadowRulesStatPrefix: authzmodel.RBACExtAuthzShadowRulesStatPrefix,
   346  	}
   347  	return []*listener.Filter{
   348  		{
   349  			Name:       wellknown.RoleBasedAccessControl,
   350  			ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
   351  		},
   352  		{
   353  			Name:       wellknown.ExternalAuthorization,
   354  			ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(extauthz.tcp)},
   355  		},
   356  	}
   357  }
   358  
   359  func getBadCustomDenyRules(rules *rbacpb.RBAC) *rbacpb.RBAC {
   360  	rbac := &rbacpb.RBAC{
   361  		Action:   rbacpb.RBAC_DENY,
   362  		Policies: map[string]*rbacpb.Policy{},
   363  	}
   364  	for _, key := range maps.Keys(rules.Policies) {
   365  		rbac.Policies[key+badCustomActionSuffix] = rules.Policies[key]
   366  	}
   367  	return rbac
   368  }
   369  
   370  func policyName(namespace, name string, rule int, option Option) string {
   371  	prefix := ""
   372  	if option.IsCustomBuilder {
   373  		prefix = extAuthzMatchPrefix + "-"
   374  	}
   375  	return fmt.Sprintf("%sns[%s]-policy[%s]-rule[%d]", prefix, namespace, name, rule)
   376  }