istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/security/authz/builder/extauthz.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  	"net/url"
    20  	"sort"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"github.com/davecgh/go-spew/spew"
    25  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    26  	extauthzhttp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
    27  	extauthztcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ext_authz/v3"
    28  	envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
    29  	envoytypev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
    30  	"github.com/hashicorp/go-multierror"
    31  	"google.golang.org/protobuf/types/known/durationpb"
    32  
    33  	meshconfig "istio.io/api/mesh/v1alpha1"
    34  	"istio.io/istio/pilot/pkg/model"
    35  	authzmodel "istio.io/istio/pilot/pkg/security/authz/model"
    36  	"istio.io/istio/pkg/config/validation/agent"
    37  	"istio.io/istio/pkg/maps"
    38  	"istio.io/istio/pkg/wellknown"
    39  )
    40  
    41  const (
    42  	extAuthzMatchPrefix   = "istio-ext-authz"
    43  	badCustomActionSuffix = `-deny-due-to-bad-CUSTOM-action`
    44  )
    45  
    46  var supportedStatus = func() []int {
    47  	var supported []int
    48  	for code := range envoytypev3.StatusCode_name {
    49  		supported = append(supported, int(code))
    50  	}
    51  	sort.Ints(supported)
    52  	return supported
    53  }()
    54  
    55  type builtExtAuthz struct {
    56  	http *extauthzhttp.ExtAuthz
    57  	tcp  *extauthztcp.ExtAuthz
    58  	err  error
    59  }
    60  
    61  func processExtensionProvider(push *model.PushContext) map[string]*builtExtAuthz {
    62  	resolved := map[string]*builtExtAuthz{}
    63  	for i, config := range push.Mesh.ExtensionProviders {
    64  		var errs error
    65  		if config.Name == "" {
    66  			errs = multierror.Append(errs, fmt.Errorf("extension provider name must not be empty, found empty at index: %d", i))
    67  		} else if _, found := resolved[config.Name]; found {
    68  			errs = multierror.Append(errs, fmt.Errorf("extension provider name must be unique, found duplicate: %s", config.Name))
    69  		}
    70  		var parsed *builtExtAuthz
    71  		var err error
    72  		// TODO(yangminzhu): Refactor and cache the ext_authz config.
    73  		switch p := config.Provider.(type) {
    74  		case *meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp:
    75  			if err = agent.ValidateExtensionProviderEnvoyExtAuthzHTTP(p.EnvoyExtAuthzHttp); err == nil {
    76  				parsed, err = buildExtAuthzHTTP(push, p.EnvoyExtAuthzHttp)
    77  			}
    78  		case *meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc:
    79  			if err = agent.ValidateExtensionProviderEnvoyExtAuthzGRPC(p.EnvoyExtAuthzGrpc); err == nil {
    80  				parsed, err = buildExtAuthzGRPC(push, p.EnvoyExtAuthzGrpc)
    81  			}
    82  		default:
    83  			continue
    84  		}
    85  		if err != nil {
    86  			errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("failed to parse extension provider %q:", config.Name)))
    87  		}
    88  		if parsed == nil {
    89  			parsed = &builtExtAuthz{}
    90  		}
    91  		parsed.err = errs
    92  		resolved[config.Name] = parsed
    93  	}
    94  
    95  	if authzLog.DebugEnabled() {
    96  		authzLog.Debugf("Resolved extension providers: %v", spew.Sdump(resolved))
    97  	}
    98  	return resolved
    99  }
   100  
   101  func notAllTheSame(names []string) bool {
   102  	for i := 1; i < len(names); i++ {
   103  		if names[i-1] != names[i] {
   104  			return true
   105  		}
   106  	}
   107  	return false
   108  }
   109  
   110  func getExtAuthz(resolved map[string]*builtExtAuthz, providers []string) (*builtExtAuthz, error) {
   111  	if resolved == nil {
   112  		return nil, fmt.Errorf("extension provider is either invalid or undefined")
   113  	}
   114  	if len(providers) < 1 {
   115  		return nil, fmt.Errorf("no provider specified in authorization policy")
   116  	}
   117  	if notAllTheSame(providers) {
   118  		return nil, fmt.Errorf("only 1 provider can be used per workload, found multiple providers: %v", providers)
   119  	}
   120  
   121  	provider := providers[0]
   122  	ret, found := resolved[provider]
   123  	if !found {
   124  		var li []string
   125  		for p := range resolved {
   126  			li = append(li, p)
   127  		}
   128  		return nil, fmt.Errorf("available providers are %v but found %q", li, provider)
   129  	} else if ret.err != nil {
   130  		return nil, fmt.Errorf("found errors in provider %s: %v", provider, ret.err)
   131  	}
   132  
   133  	return ret, nil
   134  }
   135  
   136  func buildExtAuthzHTTP(push *model.PushContext,
   137  	config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider,
   138  ) (*builtExtAuthz, error) {
   139  	var errs error
   140  	port, err := parsePort(config.Port)
   141  	if err != nil {
   142  		errs = multierror.Append(errs, err)
   143  	}
   144  	hostname, cluster, err := model.LookupCluster(push, config.Service, port)
   145  	if err != nil {
   146  		model.IncLookupClusterFailures("authz")
   147  		errs = multierror.Append(errs, err)
   148  	}
   149  	status, err := parseStatusOnError(config.StatusOnError)
   150  	if err != nil {
   151  		errs = multierror.Append(errs, err)
   152  	}
   153  	if config.PathPrefix != "" {
   154  		if _, err := url.Parse(config.PathPrefix); err != nil {
   155  			errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid pathPrefix %q:", config.PathPrefix)))
   156  		}
   157  		if !strings.HasPrefix(config.PathPrefix, "/") {
   158  			errs = multierror.Append(errs, fmt.Errorf("pathPrefix must begin with `/`, found: %q", config.PathPrefix))
   159  		}
   160  	}
   161  	checkWildcard := func(field string, values []string) {
   162  		for _, val := range values {
   163  			if val == "*" {
   164  				errs = multierror.Append(errs, fmt.Errorf("a single wildcard (\"*\") is not supported, change it to either prefix or suffix match: %s", field))
   165  			}
   166  		}
   167  	}
   168  	checkWildcard("IncludeRequestHeadersInCheck", config.IncludeRequestHeadersInCheck)
   169  	//nolint: staticcheck
   170  	checkWildcard("IncludeHeadersInCheck", config.IncludeHeadersInCheck)
   171  	checkWildcard("HeadersToDownstreamOnDeny", config.HeadersToDownstreamOnDeny)
   172  	checkWildcard("HeadersToDownstreamOnAllow", config.HeadersToDownstreamOnAllow)
   173  	checkWildcard("HeadersToUpstreamOnAllow", config.HeadersToUpstreamOnAllow)
   174  
   175  	if errs != nil {
   176  		return nil, errs
   177  	}
   178  
   179  	return generateHTTPConfig(hostname, cluster, status, config), nil
   180  }
   181  
   182  func buildExtAuthzGRPC(push *model.PushContext,
   183  	config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider,
   184  ) (*builtExtAuthz, error) {
   185  	var errs error
   186  	port, err := parsePort(config.Port)
   187  	if err != nil {
   188  		errs = multierror.Append(errs, err)
   189  	}
   190  	hostname, cluster, err := model.LookupCluster(push, config.Service, port)
   191  	if err != nil {
   192  		errs = multierror.Append(errs, err)
   193  	}
   194  	status, err := parseStatusOnError(config.StatusOnError)
   195  	if err != nil {
   196  		errs = multierror.Append(errs, err)
   197  	}
   198  	if errs != nil {
   199  		return nil, errs
   200  	}
   201  
   202  	return generateGRPCConfig(cluster, hostname, config, status), nil
   203  }
   204  
   205  func parsePort(port uint32) (int, error) {
   206  	if 1 <= port && port <= 65535 {
   207  		return int(port), nil
   208  	}
   209  	return 0, fmt.Errorf("port must be in the range [1, 65535], found: %d", port)
   210  }
   211  
   212  func parseStatusOnError(status string) (*envoytypev3.HttpStatus, error) {
   213  	if status == "" {
   214  		return nil, nil
   215  	}
   216  	code, err := strconv.ParseInt(status, 10, 32)
   217  	if err != nil {
   218  		return nil, multierror.Prefix(err, fmt.Sprintf("invalid statusOnError %q:", status))
   219  	}
   220  	if _, found := envoytypev3.StatusCode_name[int32(code)]; !found {
   221  		return nil, fmt.Errorf("unsupported statusOnError %s, supported values: %v", status, supportedStatus)
   222  	}
   223  	return &envoytypev3.HttpStatus{Code: envoytypev3.StatusCode(code)}, nil
   224  }
   225  
   226  func generateHTTPConfig(hostname, cluster string, status *envoytypev3.HttpStatus,
   227  	config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider,
   228  ) *builtExtAuthz {
   229  	service := &extauthzhttp.HttpService{
   230  		PathPrefix: config.PathPrefix,
   231  		ServerUri: &core.HttpUri{
   232  			// Timeout is required.
   233  			Timeout: timeoutOrDefault(config.Timeout),
   234  			// Uri is required but actually not used in the ext_authz filter.
   235  			Uri: fmt.Sprintf("http://%s", hostname),
   236  			HttpUpstreamType: &core.HttpUri_Cluster{
   237  				Cluster: cluster,
   238  			},
   239  		},
   240  	}
   241  	allowedHeaders := generateHeaders(config.IncludeRequestHeadersInCheck)
   242  	if allowedHeaders == nil {
   243  		// IncludeHeadersInCheck is deprecated, only use it if IncludeRequestHeadersInCheck is not set.
   244  		// TODO: Remove the IncludeHeadersInCheck field before promoting to beta.
   245  		//nolint: staticcheck
   246  		allowedHeaders = generateHeaders(config.IncludeHeadersInCheck)
   247  	}
   248  	var headersToAdd []*core.HeaderValue
   249  	additionalHeaders := maps.Keys(config.IncludeAdditionalHeadersInCheck)
   250  	sort.Strings(additionalHeaders)
   251  	for _, k := range additionalHeaders {
   252  		headersToAdd = append(headersToAdd, &core.HeaderValue{
   253  			Key:   k,
   254  			Value: config.IncludeAdditionalHeadersInCheck[k],
   255  		})
   256  	}
   257  	if len(headersToAdd) != 0 {
   258  		service.AuthorizationRequest = &extauthzhttp.AuthorizationRequest{
   259  			HeadersToAdd: headersToAdd,
   260  		}
   261  	}
   262  
   263  	if len(config.HeadersToUpstreamOnAllow) > 0 || len(config.HeadersToDownstreamOnDeny) > 0 ||
   264  		len(config.HeadersToDownstreamOnAllow) > 0 {
   265  		service.AuthorizationResponse = &extauthzhttp.AuthorizationResponse{
   266  			AllowedUpstreamHeaders:        generateHeaders(config.HeadersToUpstreamOnAllow),
   267  			AllowedClientHeaders:          generateHeaders(config.HeadersToDownstreamOnDeny),
   268  			AllowedClientHeadersOnSuccess: generateHeaders(config.HeadersToDownstreamOnAllow),
   269  		}
   270  	}
   271  	http := &extauthzhttp.ExtAuthz{
   272  		StatusOnError:       status,
   273  		FailureModeAllow:    config.FailOpen,
   274  		TransportApiVersion: core.ApiVersion_V3,
   275  		Services: &extauthzhttp.ExtAuthz_HttpService{
   276  			HttpService: service,
   277  		},
   278  		FilterEnabledMetadata: generateFilterMatcher(wellknown.HTTPRoleBasedAccessControl),
   279  		WithRequestBody:       withBodyRequest(config.IncludeRequestBodyInCheck),
   280  	}
   281  	if allowedHeaders != nil {
   282  		http.AllowedHeaders = allowedHeaders
   283  	}
   284  	return &builtExtAuthz{http: http}
   285  }
   286  
   287  func generateGRPCConfig(
   288  	cluster string,
   289  	hostname string,
   290  	config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider,
   291  	status *envoytypev3.HttpStatus,
   292  ) *builtExtAuthz {
   293  	grpc := &core.GrpcService{
   294  		TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
   295  			EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
   296  				ClusterName: cluster,
   297  				Authority:   hostname,
   298  			},
   299  		},
   300  		Timeout: timeoutOrDefault(config.Timeout),
   301  	}
   302  	http := &extauthzhttp.ExtAuthz{
   303  		StatusOnError:    status,
   304  		FailureModeAllow: config.FailOpen,
   305  		Services: &extauthzhttp.ExtAuthz_GrpcService{
   306  			GrpcService: grpc,
   307  		},
   308  		FilterEnabledMetadata: generateFilterMatcher(wellknown.HTTPRoleBasedAccessControl),
   309  		TransportApiVersion:   core.ApiVersion_V3,
   310  		WithRequestBody:       withBodyRequest(config.IncludeRequestBodyInCheck),
   311  	}
   312  	tcp := &extauthztcp.ExtAuthz{
   313  		StatPrefix:            "tcp.",
   314  		FailureModeAllow:      config.FailOpen,
   315  		TransportApiVersion:   core.ApiVersion_V3,
   316  		GrpcService:           grpc,
   317  		FilterEnabledMetadata: generateFilterMatcher(wellknown.RoleBasedAccessControl),
   318  	}
   319  	return &builtExtAuthz{http: http, tcp: tcp}
   320  }
   321  
   322  func generateHeaders(headers []string) *envoy_type_matcher_v3.ListStringMatcher {
   323  	if len(headers) == 0 {
   324  		return nil
   325  	}
   326  	var patterns []*envoy_type_matcher_v3.StringMatcher
   327  	for _, header := range headers {
   328  		pattern := &envoy_type_matcher_v3.StringMatcher{
   329  			IgnoreCase: true,
   330  		}
   331  		if strings.HasPrefix(header, "*") {
   332  			pattern.MatchPattern = &envoy_type_matcher_v3.StringMatcher_Suffix{
   333  				Suffix: strings.TrimPrefix(header, "*"),
   334  			}
   335  		} else if strings.HasSuffix(header, "*") {
   336  			pattern.MatchPattern = &envoy_type_matcher_v3.StringMatcher_Prefix{
   337  				Prefix: strings.TrimSuffix(header, "*"),
   338  			}
   339  		} else {
   340  			pattern.MatchPattern = &envoy_type_matcher_v3.StringMatcher_Exact{
   341  				Exact: header,
   342  			}
   343  		}
   344  		patterns = append(patterns, pattern)
   345  	}
   346  	return &envoy_type_matcher_v3.ListStringMatcher{Patterns: patterns}
   347  }
   348  
   349  func generateFilterMatcher(name string) *envoy_type_matcher_v3.MetadataMatcher {
   350  	return &envoy_type_matcher_v3.MetadataMatcher{
   351  		Filter: name,
   352  		Path: []*envoy_type_matcher_v3.MetadataMatcher_PathSegment{
   353  			{
   354  				Segment: &envoy_type_matcher_v3.MetadataMatcher_PathSegment_Key{
   355  					Key: authzmodel.RBACExtAuthzShadowRulesStatPrefix + authzmodel.RBACShadowEffectivePolicyID,
   356  				},
   357  			},
   358  		},
   359  		Value: &envoy_type_matcher_v3.ValueMatcher{
   360  			MatchPattern: &envoy_type_matcher_v3.ValueMatcher_StringMatch{
   361  				StringMatch: &envoy_type_matcher_v3.StringMatcher{
   362  					MatchPattern: &envoy_type_matcher_v3.StringMatcher_Prefix{
   363  						Prefix: extAuthzMatchPrefix,
   364  					},
   365  				},
   366  			},
   367  		},
   368  	}
   369  }
   370  
   371  func timeoutOrDefault(t *durationpb.Duration) *durationpb.Duration {
   372  	if t == nil {
   373  		// Default timeout is 600s.
   374  		return &durationpb.Duration{Seconds: 600}
   375  	}
   376  	return t
   377  }
   378  
   379  func withBodyRequest(config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationRequestBody) *extauthzhttp.BufferSettings {
   380  	if config == nil {
   381  		return nil
   382  	}
   383  	return &extauthzhttp.BufferSettings{
   384  		MaxRequestBytes:     config.MaxRequestBytes,
   385  		AllowPartialMessage: config.AllowPartialMessage,
   386  		PackAsBytes:         config.PackAsBytes,
   387  	}
   388  }