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 }