mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-21 13:36:41 +00:00
* fix(controller): decode old object for delete requests Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix: preserve ca-bundles injected from external providers Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
205 lines
4.8 KiB
Go
205 lines
4.8 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package validation
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
apirules "github.com/projectcapsule/capsule/pkg/api/rules"
|
|
ruleengine "github.com/projectcapsule/capsule/pkg/ruleengine"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
)
|
|
|
|
type loadBalancerCIDRRule struct {
|
|
CIDRs []string
|
|
}
|
|
|
|
func (h *serviceRules) validateLoadBalancers(
|
|
svc *corev1.Service,
|
|
enforceBodies []*apirules.NamespaceRuleEnforceBody,
|
|
) (*ruleengine.Evaluation, error) {
|
|
if svc == nil || svc.Spec.Type != corev1.ServiceTypeLoadBalancer {
|
|
return nil, nil
|
|
}
|
|
|
|
if requiresLoadBalancerCIDRs(enforceBodies) && len(loadBalancerCIDRValues(svc)) == 0 {
|
|
return &ruleengine.Evaluation{
|
|
Blocking: &ruleengine.Decision{
|
|
SetName: "loadBalancer CIDR",
|
|
EventReason: events.ReasonForbiddenLoadBalancerCIDR,
|
|
Action: apirules.ActionTypeDeny,
|
|
Value: ruleengine.Value{
|
|
Value: string(corev1.ServiceTypeLoadBalancer),
|
|
Path: "spec.type",
|
|
},
|
|
Message: "loadBalancer service requires spec.loadBalancerIP or spec.loadBalancerSourceRanges because loadBalancer CIDR constraints are enforced by namespace rule",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
values := loadBalancerCIDRValues(svc)
|
|
if len(values) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return evaluateServiceRules[loadBalancerCIDRRule](
|
|
svc,
|
|
enforceBodies,
|
|
serviceRuleSet[loadBalancerCIDRRule]{
|
|
Name: "loadBalancer CIDR",
|
|
EventReason: events.ReasonForbiddenLoadBalancerCIDR,
|
|
Values: func(_ *corev1.Service) []ruleengine.Value {
|
|
return values
|
|
},
|
|
Rules: func(enforce *apirules.NamespaceRuleEnforceBody) []loadBalancerCIDRRule {
|
|
if enforce == nil ||
|
|
enforce.Services.LoadBalancers == nil ||
|
|
len(enforce.Services.LoadBalancers.CIDRs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
cidrs := make([]string, 0, len(enforce.Services.LoadBalancers.CIDRs))
|
|
for _, cidr := range enforce.Services.LoadBalancers.CIDRs {
|
|
cidr = strings.TrimSpace(cidr)
|
|
if cidr == "" {
|
|
continue
|
|
}
|
|
|
|
cidrs = append(cidrs, cidr)
|
|
}
|
|
|
|
if len(cidrs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return []loadBalancerCIDRRule{
|
|
{
|
|
CIDRs: cidrs,
|
|
},
|
|
}
|
|
},
|
|
Matches: func(rule loadBalancerCIDRRule, value ruleengine.Value) (ruleengine.Match, error) {
|
|
if len(rule.CIDRs) == 0 {
|
|
return ruleengine.Match{}, nil
|
|
}
|
|
|
|
for _, rawCIDR := range rule.CIDRs {
|
|
allowedCIDR, err := parseCIDR(rawCIDR)
|
|
if err != nil {
|
|
return ruleengine.Match{}, fmt.Errorf("invalid loadBalancer CIDR %q: %w", rawCIDR, err)
|
|
}
|
|
|
|
if ip := net.ParseIP(value.Value); ip != nil {
|
|
if !cidrContainsIP(allowedCIDR, ip) {
|
|
continue
|
|
}
|
|
|
|
return ruleengine.Match{
|
|
Matched: true,
|
|
MatchedValue: rawCIDR,
|
|
Detail: fmt.Sprintf("%s is contained in %s", value.Value, rawCIDR),
|
|
}, nil
|
|
}
|
|
|
|
_, requestedCIDR, err := net.ParseCIDR(value.Value)
|
|
if err != nil {
|
|
return ruleengine.Match{}, fmt.Errorf(
|
|
"%s contains invalid IP or CIDR %q: %w",
|
|
value.Path,
|
|
value.Value,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if !cidrContainsCIDR(allowedCIDR, requestedCIDR) {
|
|
continue
|
|
}
|
|
|
|
return ruleengine.Match{
|
|
Matched: true,
|
|
MatchedValue: rawCIDR,
|
|
Detail: fmt.Sprintf("%s is contained in %s", value.Value, rawCIDR),
|
|
}, nil
|
|
}
|
|
|
|
return ruleengine.Match{
|
|
Matched: false,
|
|
}, nil
|
|
},
|
|
RuleDescription: func(rule loadBalancerCIDRRule) string {
|
|
return strings.Join(rule.CIDRs, ", ")
|
|
},
|
|
AllowedDescription: "Allowed CIDRs",
|
|
},
|
|
)
|
|
}
|
|
|
|
func requiresLoadBalancerCIDRs(
|
|
enforceBodies []*apirules.NamespaceRuleEnforceBody,
|
|
) bool {
|
|
for _, enforce := range enforceBodies {
|
|
if enforce == nil ||
|
|
enforce.Services.LoadBalancers == nil {
|
|
continue
|
|
}
|
|
|
|
if len(enforce.Services.LoadBalancers.CIDRs) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func parseCIDR(raw string) (*net.IPNet, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil, fmt.Errorf("CIDR is empty")
|
|
}
|
|
|
|
if !strings.Contains(raw, "/") {
|
|
ip := net.ParseIP(raw)
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("invalid CIDR %q", raw)
|
|
}
|
|
|
|
if ip.To4() != nil {
|
|
raw += "/32"
|
|
} else {
|
|
raw += "/128"
|
|
}
|
|
}
|
|
|
|
_, network, err := net.ParseCIDR(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return network, nil
|
|
}
|
|
|
|
func loadBalancerCIDRValues(svc *corev1.Service) []ruleengine.Value {
|
|
out := make([]ruleengine.Value, 0, 1+len(svc.Spec.LoadBalancerSourceRanges))
|
|
|
|
if svc.Spec.LoadBalancerIP != "" {
|
|
out = append(out, ruleengine.Value{
|
|
Value: svc.Spec.LoadBalancerIP,
|
|
Path: "spec.loadBalancerIP",
|
|
})
|
|
}
|
|
|
|
for i, sourceRange := range svc.Spec.LoadBalancerSourceRanges {
|
|
out = append(out, ruleengine.Value{
|
|
Value: sourceRange,
|
|
Path: fmt.Sprintf("spec.loadBalancerSourceRanges[%d]", i),
|
|
})
|
|
}
|
|
|
|
return out
|
|
}
|