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 }