istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/ambient/authorization.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 ambient 16 17 import ( 18 "fmt" 19 "net/netip" 20 "strconv" 21 "strings" 22 23 "istio.io/api/security/v1beta1" 24 securityclient "istio.io/client-go/pkg/apis/security/v1beta1" 25 "istio.io/istio/pilot/pkg/model" 26 "istio.io/istio/pkg/config/schema/kind" 27 "istio.io/istio/pkg/log" 28 "istio.io/istio/pkg/util/sets" 29 "istio.io/istio/pkg/workloadapi/security" 30 ) 31 32 const ( 33 staticStrictPolicyName = "istio_converted_static_strict" // use '_' character since those are illegal in k8s names 34 ) 35 36 func (a *index) Policies(requested sets.Set[model.ConfigKey]) []model.WorkloadAuthorization { 37 // TODO: use many Gets instead of List? 38 cfgs := a.authorizationPolicies.List() 39 l := len(cfgs) 40 if len(requested) > 0 { 41 l = len(requested) 42 } 43 res := make([]model.WorkloadAuthorization, 0, l) 44 for _, cfg := range cfgs { 45 k := model.ConfigKey{ 46 Kind: kind.AuthorizationPolicy, 47 Name: cfg.Authorization.Name, 48 Namespace: cfg.Authorization.Namespace, 49 } 50 51 if len(requested) > 0 && !requested.Contains(k) { 52 continue 53 } 54 res = append(res, cfg) 55 } 56 return res 57 } 58 59 // convertedSelectorPeerAuthentications returns a list of keys corresponding to one or both of: 60 // [static STRICT policy, port-level STRICT policy] based on the effective PeerAuthentication policy 61 func convertedSelectorPeerAuthentications(rootNamespace string, configs []*securityclient.PeerAuthentication) []string { 62 var meshCfg, namespaceCfg, workloadCfg *securityclient.PeerAuthentication 63 for _, cfg := range configs { 64 spec := &cfg.Spec 65 if spec.Selector == nil || len(spec.Selector.MatchLabels) == 0 { 66 // Namespace-level or mesh-level policy 67 if cfg.Namespace == rootNamespace { 68 if meshCfg == nil || cfg.CreationTimestamp.Before(&meshCfg.CreationTimestamp) { 69 log.Debugf("Switch selected mesh policy to %s.%s (%v)", cfg.Name, cfg.Namespace, cfg.CreationTimestamp) 70 meshCfg = cfg 71 } 72 } else { 73 if namespaceCfg == nil || cfg.CreationTimestamp.Before(&namespaceCfg.CreationTimestamp) { 74 log.Debugf("Switch selected namespace policy to %s.%s (%v)", cfg.Name, cfg.Namespace, cfg.CreationTimestamp) 75 namespaceCfg = cfg 76 } 77 } 78 } else if cfg.Namespace != rootNamespace { 79 if workloadCfg == nil || cfg.CreationTimestamp.Before(&workloadCfg.CreationTimestamp) { 80 log.Debugf("Switch selected workload policy to %s.%s (%v)", cfg.Name, cfg.Namespace, cfg.CreationTimestamp) 81 workloadCfg = cfg 82 } 83 } 84 } 85 86 // Whether it comes from a mesh-wide, namespace-wide, or workload-specific policy 87 // if the effective policy is STRICT, then reference our static STRICT policy 88 var isEffectiveStrictPolicy bool 89 // Only 1 per port workload policy can be effective at a time. In the case of a conflict 90 // the oldest policy wins. 91 var effectivePortLevelPolicyKey string 92 93 // Process in mesh, namespace, workload order to resolve inheritance (UNSET) 94 if meshCfg != nil { 95 if !isMtlsModeUnset(meshCfg.Spec.Mtls) { 96 isEffectiveStrictPolicy = isMtlsModeStrict(meshCfg.Spec.Mtls) 97 } 98 } 99 100 if namespaceCfg != nil { 101 if !isMtlsModeUnset(namespaceCfg.Spec.Mtls) { 102 isEffectiveStrictPolicy = isMtlsModeStrict(namespaceCfg.Spec.Mtls) 103 } 104 } 105 106 if workloadCfg == nil { 107 return effectivePeerAuthenticationKeys(rootNamespace, isEffectiveStrictPolicy, "") 108 } 109 110 workloadSpec := &workloadCfg.Spec 111 112 // Regardless of if we have port-level overrides, if the workload policy is STRICT, then we need to reference our static STRICT policy 113 if isMtlsModeStrict(workloadSpec.Mtls) { 114 isEffectiveStrictPolicy = true 115 } 116 117 // Regardless of if we have port-level overrides, if the workload policy is PERMISSIVE or DISABLE, then we shouldn't send our static STRICT policy 118 if isMtlsModePermissive(workloadSpec.Mtls) || isMtlsModeDisable(workloadSpec.Mtls) { 119 isEffectiveStrictPolicy = false 120 } 121 122 if workloadSpec.PortLevelMtls != nil { 123 switch workloadSpec.GetMtls().GetMode() { 124 case v1beta1.PeerAuthentication_MutualTLS_STRICT: 125 foundPermissive := false 126 for _, portMtls := range workloadSpec.PortLevelMtls { 127 if isMtlsModePermissive(portMtls) || isMtlsModeDisable(portMtls) { 128 foundPermissive = true 129 break 130 } 131 } 132 133 if foundPermissive { 134 // If we found a non-strict policy, we need to reference this workload policy to see the port level exceptions 135 effectivePortLevelPolicyKey = workloadCfg.Namespace + "/" + model.GetAmbientPolicyConfigName(model.ConfigKey{ 136 Name: workloadCfg.Name, 137 Kind: kind.PeerAuthentication, 138 Namespace: workloadCfg.Namespace, 139 }) 140 isEffectiveStrictPolicy = false // don't send our static STRICT policy since the converted form of this policy will include the default STRICT mode 141 } 142 case v1beta1.PeerAuthentication_MutualTLS_PERMISSIVE, v1beta1.PeerAuthentication_MutualTLS_DISABLE: 143 foundStrict := false 144 for _, portMtls := range workloadSpec.PortLevelMtls { 145 if isMtlsModeStrict(portMtls) { 146 foundStrict = true 147 break 148 } 149 } 150 151 // There's a STRICT port mode, so we need to reference this policy in the workload 152 if foundStrict { 153 effectivePortLevelPolicyKey = workloadCfg.Namespace + "/" + model.GetAmbientPolicyConfigName(model.ConfigKey{ 154 Name: workloadCfg.Name, 155 Kind: kind.PeerAuthentication, 156 Namespace: workloadCfg.Namespace, 157 }) 158 } 159 default: // Unset 160 if isEffectiveStrictPolicy { 161 // Strict mesh or namespace policy 162 foundPermissive := false 163 for _, portMtls := range workloadSpec.PortLevelMtls { 164 if isMtlsModePermissive(portMtls) { 165 foundPermissive = true 166 break 167 } 168 } 169 170 if foundPermissive { 171 // If we found a non-strict policy, we need to reference this workload policy to see the port level exceptions 172 effectivePortLevelPolicyKey = workloadCfg.Namespace + "/" + model.GetAmbientPolicyConfigName(model.ConfigKey{ 173 Name: workloadCfg.Name, 174 Kind: kind.PeerAuthentication, 175 Namespace: workloadCfg.Namespace, 176 }) 177 } 178 } else { 179 // Permissive mesh or namespace policy 180 isEffectiveStrictPolicy = false // any ports that aren't specified will be PERMISSIVE so this workload isn't effectively under a STRICT policy 181 foundStrict := false 182 for _, portMtls := range workloadSpec.PortLevelMtls { 183 if isMtlsModeStrict(portMtls) { 184 foundStrict = true 185 continue 186 } 187 } 188 189 // There's a STRICT port mode, so we need to reference this policy in the workload 190 if foundStrict { 191 effectivePortLevelPolicyKey = workloadCfg.Namespace + "/" + model.GetAmbientPolicyConfigName(model.ConfigKey{ 192 Name: workloadCfg.Name, 193 Kind: kind.PeerAuthentication, 194 Namespace: workloadCfg.Namespace, 195 }) 196 } 197 } 198 } 199 } 200 201 return effectivePeerAuthenticationKeys(rootNamespace, isEffectiveStrictPolicy, effectivePortLevelPolicyKey) 202 } 203 204 func effectivePeerAuthenticationKeys(rootNamespace string, isEffectiveStringPolicy bool, effectiveWorkloadPolicyKey string) []string { 205 res := sets.New[string]() 206 207 if isEffectiveStringPolicy { 208 res.Insert(fmt.Sprintf("%s/%s", rootNamespace, staticStrictPolicyName)) 209 } 210 211 if effectiveWorkloadPolicyKey != "" { 212 res.Insert(effectiveWorkloadPolicyKey) 213 } 214 215 return sets.SortedList(res) 216 } 217 218 // convertPeerAuthentication converts a PeerAuthentication to an L4 authorization policy (i.e. security.Authorization) iff 219 // 1. the PeerAuthentication has a workload selector 220 // 2. The PeerAuthentication is NOT in the root namespace 221 // 3. There is a portLevelMtls policy (technically implied by 1) 222 // 4. If the top-level mode is PERMISSIVE or DISABLE, there is at least one portLevelMtls policy with mode STRICT 223 // 224 // STRICT policies that don't have portLevelMtls will be 225 // handled when the Workload xDS resource is pushed (a static STRICT-equivalent policy will always be pushed) 226 func convertPeerAuthentication(rootNamespace string, cfg *securityclient.PeerAuthentication) *security.Authorization { 227 pa := &cfg.Spec 228 229 mode := pa.GetMtls().GetMode() 230 231 scope := security.Scope_WORKLOAD_SELECTOR 232 // violates case #1, #2, or #3 233 if cfg.Namespace == rootNamespace || pa.Selector == nil || len(pa.PortLevelMtls) == 0 { 234 log.Debugf("skipping PeerAuthentication %s/%s for ambient since it isn't a workload policy with port level mTLS", cfg.Namespace, cfg.Name) 235 return nil 236 } 237 238 action := security.Action_DENY 239 var rules []*security.Rules 240 241 if mode == v1beta1.PeerAuthentication_MutualTLS_STRICT { 242 rules = append(rules, &security.Rules{ 243 Matches: []*security.Match{ 244 { 245 NotPrincipals: []*security.StringMatch{ 246 { 247 MatchType: &security.StringMatch_Presence{}, 248 }, 249 }, 250 }, 251 }, 252 }) 253 } 254 255 // If we have a strict policy and all of the ports are strict, it's effectively a strict policy 256 // so we can exit early and have the WorkloadRbac xDS server push its static strict policy. 257 // Note that this doesn't actually attach the policy to any workload; it just makes it available 258 // to ztunnel in case a workload needs it. 259 foundNonStrictPortmTLS := false 260 for port, mtls := range pa.PortLevelMtls { 261 switch portMtlsMode := mtls.GetMode(); { 262 case portMtlsMode == v1beta1.PeerAuthentication_MutualTLS_STRICT: 263 rules = append(rules, &security.Rules{ 264 Matches: []*security.Match{ 265 { 266 NotPrincipals: []*security.StringMatch{ 267 { 268 MatchType: &security.StringMatch_Presence{}, 269 }, 270 }, 271 DestinationPorts: []uint32{port}, 272 }, 273 }, 274 }) 275 case portMtlsMode == v1beta1.PeerAuthentication_MutualTLS_PERMISSIVE: 276 // Check top-level mode 277 if mode == v1beta1.PeerAuthentication_MutualTLS_PERMISSIVE || mode == v1beta1.PeerAuthentication_MutualTLS_DISABLE { 278 // we don't care; log and continue 279 log.Debugf("skipping port %s/%s for PeerAuthentication %s/%s for ambient since the parent mTLS mode is %s", 280 port, portMtlsMode, cfg.Namespace, cfg.Name, mode) 281 continue 282 } 283 foundNonStrictPortmTLS = true 284 285 // If the top level policy is STRICT, we need to add a rule for the port that exempts it from the deny policy 286 rules = append(rules, &security.Rules{ 287 Matches: []*security.Match{ 288 { 289 NotDestinationPorts: []uint32{port}, // if the incoming connection does not match this port, deny (notice there's no principals requirement) 290 }, 291 }, 292 }) 293 case portMtlsMode == v1beta1.PeerAuthentication_MutualTLS_DISABLE: 294 // Check top-level mode 295 if mode == v1beta1.PeerAuthentication_MutualTLS_PERMISSIVE || mode == v1beta1.PeerAuthentication_MutualTLS_DISABLE { 296 // we don't care; log and continue 297 log.Debugf("skipping port %s/%s for PeerAuthentication %s/%s for ambient since the parent mTLS mode is %s", 298 port, portMtlsMode, cfg.Namespace, cfg.Name, mode) 299 continue 300 } 301 foundNonStrictPortmTLS = true 302 303 // If the top level policy is STRICT, we need to add a rule for the port that exempts it from the deny policy 304 rules = append(rules, &security.Rules{ 305 Matches: []*security.Match{ 306 { 307 NotDestinationPorts: []uint32{port}, // if the incoming connection does not match this port, deny (notice there's no principals requirement) 308 }, 309 }, 310 }) 311 default: 312 log.Debugf("skipping port %s for PeerAuthentication %s/%s for ambient since it is %s", port, cfg.Namespace, cfg.Name, portMtlsMode) 313 continue 314 } 315 } 316 317 // If the top level TLS mode is STRICT and all of the port level mTLS modes are STRICT, this is just a strict policy and we'll exit early 318 if mode == v1beta1.PeerAuthentication_MutualTLS_STRICT && !foundNonStrictPortmTLS { 319 return nil 320 } 321 322 if len(rules) == 0 { 323 // we never added any rules; return 324 return nil 325 } 326 327 opol := &security.Authorization{ 328 Name: model.GetAmbientPolicyConfigName(model.ConfigKey{ 329 Name: cfg.Name, 330 Kind: kind.PeerAuthentication, 331 Namespace: cfg.Namespace, 332 }), 333 Namespace: cfg.Namespace, 334 Scope: scope, 335 Action: action, 336 Groups: []*security.Group{{Rules: rules}}, 337 } 338 339 return opol 340 } 341 342 func convertAuthorizationPolicy(rootns string, obj *securityclient.AuthorizationPolicy) *security.Authorization { 343 pol := &obj.Spec 344 345 polTargetRef := model.GetTargetRefs(pol) 346 if len(polTargetRef) > 0 { 347 // TargetRef is not intended for ztunnel 348 return nil 349 } 350 351 scope := security.Scope_WORKLOAD_SELECTOR 352 if pol.GetSelector() == nil { 353 scope = security.Scope_NAMESPACE 354 // TODO: TDA 355 if rootns == obj.Namespace { 356 scope = security.Scope_GLOBAL // TODO: global workload? 357 } 358 } 359 action := security.Action_ALLOW 360 switch pol.Action { 361 case v1beta1.AuthorizationPolicy_ALLOW: 362 case v1beta1.AuthorizationPolicy_DENY: 363 action = security.Action_DENY 364 default: 365 return nil 366 } 367 opol := &security.Authorization{ 368 Name: obj.Name, 369 Namespace: obj.Namespace, 370 Scope: scope, 371 Action: action, 372 Groups: nil, 373 } 374 375 for _, rule := range pol.Rules { 376 rules := handleRule(action, rule) 377 if rules != nil { 378 rg := &security.Group{ 379 Rules: rules, 380 } 381 opol.Groups = append(opol.Groups, rg) 382 } 383 } 384 385 return opol 386 } 387 388 func anyNonEmpty[T any](arr ...[]T) bool { 389 for _, a := range arr { 390 if len(a) > 0 { 391 return true 392 } 393 } 394 return false 395 } 396 397 func handleRule(action security.Action, rule *v1beta1.Rule) []*security.Rules { 398 toMatches := []*security.Match{} 399 for _, to := range rule.To { 400 op := to.Operation 401 if action == security.Action_ALLOW && anyNonEmpty(op.Hosts, op.NotHosts, op.Methods, op.NotMethods, op.Paths, op.NotPaths) { 402 // L7 policies never match for ALLOW 403 // For DENY they will always match, so it is more restrictive 404 return nil 405 } 406 match := &security.Match{ 407 DestinationPorts: stringToPort(op.Ports), 408 NotDestinationPorts: stringToPort(op.NotPorts), 409 } 410 toMatches = append(toMatches, match) 411 } 412 fromMatches := []*security.Match{} 413 for _, from := range rule.From { 414 op := from.Source 415 if action == security.Action_ALLOW && anyNonEmpty(op.RemoteIpBlocks, op.NotRemoteIpBlocks, op.RequestPrincipals, op.NotRequestPrincipals) { 416 // L7 policies never match for ALLOW 417 // For DENY they will always match, so it is more restrictive 418 return nil 419 } 420 match := &security.Match{ 421 SourceIps: stringToIP(op.IpBlocks), 422 NotSourceIps: stringToIP(op.NotIpBlocks), 423 Namespaces: stringToMatch(op.Namespaces), 424 NotNamespaces: stringToMatch(op.NotNamespaces), 425 Principals: stringToMatch(op.Principals), 426 NotPrincipals: stringToMatch(op.NotPrincipals), 427 } 428 fromMatches = append(fromMatches, match) 429 } 430 431 rules := []*security.Rules{} 432 if len(toMatches) > 0 { 433 rules = append(rules, &security.Rules{Matches: toMatches}) 434 } 435 if len(fromMatches) > 0 { 436 rules = append(rules, &security.Rules{Matches: fromMatches}) 437 } 438 for _, when := range rule.When { 439 l4 := l4WhenAttributes.Contains(when.Key) 440 if action == security.Action_ALLOW && !l4 { 441 // L7 policies never match for ALLOW 442 // For DENY they will always match, so it is more restrictive 443 return nil 444 } 445 positiveMatch := &security.Match{ 446 Namespaces: whenMatch("source.namespace", when, false, stringToMatch), 447 Principals: whenMatch("source.principal", when, false, stringToMatch), 448 SourceIps: whenMatch("source.ip", when, false, stringToIP), 449 DestinationPorts: whenMatch("destination.port", when, false, stringToPort), 450 DestinationIps: whenMatch("destination.ip", when, false, stringToIP), 451 452 NotNamespaces: whenMatch("source.namespace", when, true, stringToMatch), 453 NotPrincipals: whenMatch("source.principal", when, true, stringToMatch), 454 NotSourceIps: whenMatch("source.ip", when, true, stringToIP), 455 NotDestinationPorts: whenMatch("destination.port", when, true, stringToPort), 456 NotDestinationIps: whenMatch("destination.ip", when, true, stringToIP), 457 } 458 rules = append(rules, &security.Rules{Matches: []*security.Match{positiveMatch}}) 459 } 460 return rules 461 } 462 463 var l4WhenAttributes = sets.New( 464 "source.ip", 465 "source.namespace", 466 "source.principal", 467 "destination.ip", 468 "destination.port", 469 ) 470 471 func whenMatch[T any](s string, when *v1beta1.Condition, invert bool, f func(v []string) []T) []T { 472 if when.Key != s { 473 return nil 474 } 475 if invert { 476 return f(when.NotValues) 477 } 478 return f(when.Values) 479 } 480 481 func stringToMatch(rules []string) []*security.StringMatch { 482 res := make([]*security.StringMatch, 0, len(rules)) 483 for _, v := range rules { 484 var sm *security.StringMatch 485 switch { 486 case v == "*": 487 sm = &security.StringMatch{MatchType: &security.StringMatch_Presence{}} 488 case strings.HasPrefix(v, "*"): 489 sm = &security.StringMatch{MatchType: &security.StringMatch_Suffix{ 490 Suffix: strings.TrimPrefix(v, "*"), 491 }} 492 case strings.HasSuffix(v, "*"): 493 sm = &security.StringMatch{MatchType: &security.StringMatch_Prefix{ 494 Prefix: strings.TrimSuffix(v, "*"), 495 }} 496 default: 497 sm = &security.StringMatch{MatchType: &security.StringMatch_Exact{ 498 Exact: v, 499 }} 500 } 501 res = append(res, sm) 502 } 503 return res 504 } 505 506 func stringToPort(rules []string) []uint32 { 507 res := make([]uint32, 0, len(rules)) 508 for _, m := range rules { 509 p, err := strconv.ParseUint(m, 10, 32) 510 if err != nil || p > 65535 { 511 continue 512 } 513 res = append(res, uint32(p)) 514 } 515 return res 516 } 517 518 func stringToIP(rules []string) []*security.Address { 519 res := make([]*security.Address, 0, len(rules)) 520 for _, m := range rules { 521 if len(m) == 0 { 522 continue 523 } 524 525 var ( 526 ipAddr netip.Addr 527 maxCidrPrefix uint32 528 ) 529 530 if strings.Contains(m, "/") { 531 ipp, err := netip.ParsePrefix(m) 532 if err != nil { 533 continue 534 } 535 ipAddr = ipp.Addr() 536 maxCidrPrefix = uint32(ipp.Bits()) 537 } else { 538 ipa, err := netip.ParseAddr(m) 539 if err != nil { 540 continue 541 } 542 543 ipAddr = ipa 544 maxCidrPrefix = uint32(ipAddr.BitLen()) 545 } 546 547 res = append(res, &security.Address{ 548 Address: ipAddr.AsSlice(), 549 Length: maxCidrPrefix, 550 }) 551 } 552 return res 553 } 554 555 func isMtlsModeUnset(mtls *v1beta1.PeerAuthentication_MutualTLS) bool { 556 return mtls == nil || mtls.Mode == v1beta1.PeerAuthentication_MutualTLS_UNSET 557 } 558 559 func isMtlsModeStrict(mtls *v1beta1.PeerAuthentication_MutualTLS) bool { 560 return mtls != nil && mtls.Mode == v1beta1.PeerAuthentication_MutualTLS_STRICT 561 } 562 563 func isMtlsModeDisable(mtls *v1beta1.PeerAuthentication_MutualTLS) bool { 564 return mtls != nil && mtls.Mode == v1beta1.PeerAuthentication_MutualTLS_DISABLE 565 } 566 567 func isMtlsModePermissive(mtls *v1beta1.PeerAuthentication_MutualTLS) bool { 568 return mtls != nil && mtls.Mode == v1beta1.PeerAuthentication_MutualTLS_PERMISSIVE 569 }