Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions internal/testutil/fixture/azure_securitygroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,41 @@ func (f *AzureFixture) DenyAllSecurityRule(ipFamily iputil.Family) *AzureDenyAll
}
}

func (f *AzureFixture) DenyBlockedIPRangeSecurityRule(
protocol armnetwork.SecurityRuleProtocol,
ipFamily iputil.Family,
srcPrefixes []string,
dstPorts []int32,
) *AzureDenyBlockedIPRangeSecurityRuleFixture {
name := securitygroup.GenerateDenyBlockedSecurityRuleName(protocol, ipFamily, srcPrefixes, dstPorts)
dstPortRanges := fnutil.Map(func(p int32) string { return strconv.FormatInt(int64(p), 10) }, dstPorts)
sort.Strings(dstPortRanges)

rule := &armnetwork.SecurityRule{
Name: ptr.To(name),
Properties: &armnetwork.SecurityRulePropertiesFormat{
Protocol: to.Ptr(protocol),
Access: to.Ptr(armnetwork.SecurityRuleAccessDeny),
Direction: to.Ptr(armnetwork.SecurityRuleDirectionInbound),
SourcePortRange: ptr.To("*"),
DestinationPortRanges: to.SliceOfPtrs(dstPortRanges...),
Priority: ptr.To(int32(consts.IPPrefixBlockingMinimumPriority)),
},
}
if len(dstPorts) == 0 {
rule.Properties.DestinationPortRange = ptr.To("*")
rule.Properties.DestinationPortRanges = nil
}

if len(srcPrefixes) == 1 {
rule.Properties.SourceAddressPrefix = ptr.To(srcPrefixes[0])
} else {
rule.Properties.SourceAddressPrefixes = to.SliceOfPtrs(srcPrefixes...)
}

return &AzureDenyBlockedIPRangeSecurityRuleFixture{rule: rule}
}

// AzureSecurityGroupFixture is a fixture for an Azure security group.
type AzureSecurityGroupFixture struct {
sg *armnetwork.SecurityGroup
Expand Down Expand Up @@ -231,3 +266,26 @@ func (f *AzureDenyAllSecurityRuleFixture) WithDestination(prefixes ...string) *A
func (f *AzureDenyAllSecurityRuleFixture) Build() *armnetwork.SecurityRule {
return f.rule
}

// AzureDenyBlockedIPRangeSecurityRuleFixture is a fixture for a deny blocked IP range security rule.
type AzureDenyBlockedIPRangeSecurityRuleFixture struct {
rule *armnetwork.SecurityRule
}

func (f *AzureDenyBlockedIPRangeSecurityRuleFixture) WithPriority(p int32) *AzureDenyBlockedIPRangeSecurityRuleFixture {
f.rule.Properties.Priority = ptr.To(p)
return f
}

func (f *AzureDenyBlockedIPRangeSecurityRuleFixture) WithDestination(prefixes ...string) *AzureDenyBlockedIPRangeSecurityRuleFixture {
if len(prefixes) == 1 {
f.rule.Properties.DestinationAddressPrefix = ptr.To(prefixes[0])
f.rule.Properties.DestinationAddressPrefixes = nil
} else if len(prefixes) > 1 {
f.rule.Properties.DestinationAddressPrefix = nil
f.rule.Properties.DestinationAddressPrefixes = to.SliceOfPtrs(securitygroup.NormalizeSecurityRuleAddressPrefixes(prefixes)...)
}
return f
}

func (f *AzureDenyBlockedIPRangeSecurityRuleFixture) Build() *armnetwork.SecurityRule { return f.rule }
5 changes: 5 additions & 0 deletions internal/testutil/fixture/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ func (f *KubernetesServiceFixture) WithAllowedIPRanges(parts ...string) *Kuberne
return f
}

func (f *KubernetesServiceFixture) WithBlockedIPRanges(parts ...string) *KubernetesServiceFixture {
f.svc.Annotations[consts.ServiceAnnotationBlockedIPRanges] = strings.Join(parts, ",")
return f
}

func (f *KubernetesServiceFixture) WithAllowedServiceTags(parts ...string) *KubernetesServiceFixture {
f.svc.Annotations[consts.ServiceAnnotationAllowedServiceTags] = strings.Join(parts, ",")
return f
Expand Down
10 changes: 10 additions & 0 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ const (
// It is compatible with both IPv4 and IPV6 CIDR formats.
ServiceAnnotationAllowedIPRanges = "service.beta.kubernetes.io/azure-allowed-ip-ranges"

// ServiceAnnotationBlockedIPRanges is the annotation used on the service
// to specify a list of blocked IP Ranges separated by comma.
// It is compatible with both IPv4 and IPV6 CIDR formats.
ServiceAnnotationBlockedIPRanges = "service.beta.kubernetes.io/azure-blocked-ip-ranges"

// ServiceAnnotationDenyAllExceptLoadBalancerSourceRanges denies all traffic to the load balancer except those
// within the service.Spec.LoadBalancerSourceRanges. Ref: https://github.com/kubernetes-sigs/cloud-provider-azure/issues/374.
ServiceAnnotationDenyAllExceptLoadBalancerSourceRanges = "service.beta.kubernetes.io/azure-deny-all-except-load-balancer-source-ranges"
Expand Down Expand Up @@ -355,6 +360,11 @@ const (
// TrueAnnotationValue is the true annotation value
TrueAnnotationValue = "true"

// IP prefix blocking range minimum priority
IPPrefixBlockingMinimumPriority = 400
// IP prefix blocking range maximum priority
IPPrefixBlockingMaximumPriority = 499

// LoadBalancerMinimumPriority is the minimum priority
LoadBalancerMinimumPriority = 500
// LoadBalancerMaximumPriority is the maximum priority
Expand Down
37 changes: 36 additions & 1 deletion pkg/provider/loadbalancer/accesscontrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package loadbalancer
import (
"fmt"
"net/netip"
"slices"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
Expand Down Expand Up @@ -46,6 +47,7 @@ type AccessControl struct {
// immutable pre-compute states.
SourceRanges []netip.Prefix
AllowedIPRanges []netip.Prefix
BlockedIPRanges []netip.Prefix
AllowedServiceTags []string
invalidRanges []string
securityRuleDestinationPortsByProtocol map[armnetwork.SecurityRuleProtocol][]int32
Expand Down Expand Up @@ -95,6 +97,13 @@ func NewAccessControl(logger logr.Logger, svc *v1.Service, sg *armnetwork.Securi
// Backward compatibility: no error but emit a warning event.
eventEmitter(svc, v1.EventTypeWarning, "InvalidAllowedIPRanges", EventMessageOfInvalidAllowedIPRanges(invalidAllowedIPRanges))
}
blockedIPRanges, invalidBlockedIPRanges, err := BlockedIPRanges(svc)
if err != nil {
logger.Error(err, "Failed to parse BlockedIPRanges configuration")

// Backward compatibility: no error but emit a warning event.
eventEmitter(svc, v1.EventTypeWarning, "InvalidBlockedIPRanges", EventMessageOfInvalidBlockedIPRanges(invalidBlockedIPRanges))
}
allowedServiceTags := AllowedServiceTags(svc)
securityRuleDestinationPortsByProtocol, err := SecurityRuleDestinationPortsByProtocol(svc)
if err != nil {
Expand All @@ -115,14 +124,17 @@ func NewAccessControl(logger logr.Logger, svc *v1.Service, sg *armnetwork.Securi
eventEmitter(svc, v1.EventTypeWarning, "ConflictConfiguration", EventMessageOfConflictLoadBalancerSourceRangesAndAllowedIPRanges())
}

invalidRanges := slices.Concat(invalidSourceRanges, invalidAllowedIPRanges)

return &AccessControl{
logger: logger,
svc: svc,
sgHelper: sgHelper,
SourceRanges: sourceRanges,
AllowedIPRanges: allowedIPRanges,
BlockedIPRanges: blockedIPRanges,
AllowedServiceTags: allowedServiceTags,
invalidRanges: append(invalidSourceRanges, invalidAllowedIPRanges...),
invalidRanges: invalidRanges,
securityRuleDestinationPortsByProtocol: securityRuleDestinationPortsByProtocol,
}, nil
}
Expand Down Expand Up @@ -233,11 +245,34 @@ func (ac *AccessControl) PatchSecurityGroup(dstIPv4Addresses, dstIPv6Addresses [
armnetwork.SecurityRuleProtocolAsterisk,
}

// First, add deny rules for blocked IP ranges (higher precedence) per IP family.
if len(ac.BlockedIPRanges) > 0 {
for _, protocol := range protocols {
dstPorts, found := ac.securityRuleDestinationPortsByProtocol[protocol]
if !found {
continue
}
blockedAggregated := iputil.AggregatePrefixes(ac.BlockedIPRanges)
blockedIPv4, blockedIPv6 := iputil.GroupPrefixesByFamily(blockedAggregated)
if len(blockedIPv4) > 0 && len(dstIPv4Addresses) > 0 {
if err := ac.sgHelper.AddRuleForBlockedIPRanges(blockedIPv4, protocol, dstIPv4Addresses, dstPorts); err != nil {
return fmt.Errorf("failed to add a new rule for blocked IPv4 ranges: %w", err)
}
}
if len(blockedIPv6) > 0 && len(dstIPv6Addresses) > 0 {
if err := ac.sgHelper.AddRuleForBlockedIPRanges(blockedIPv6, protocol, dstIPv6Addresses, dstPorts); err != nil {
return fmt.Errorf("failed to add a new rule for blocked IPv6 ranges: %w", err)
}
}
}
}

for _, protocol := range protocols {
dstPorts, found := ac.securityRuleDestinationPortsByProtocol[protocol]
if !found {
continue
}

if len(dstIPv4Addresses) > 0 {
for _, tag := range allowedServiceTags {
err := ac.sgHelper.AddRuleForAllowedServiceTag(tag, protocol, dstIPv4Addresses, dstPorts)
Expand Down
Loading