Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 44 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 All @@ -36,6 +37,9 @@ var (
ErrSetBothLoadBalancerSourceRangesAndAllowedIPRanges = fmt.Errorf(
"cannot set both spec.LoadBalancerSourceRanges and service annotation %s", consts.ServiceAnnotationAllowedIPRanges,
)
ErrSetBothAllowedAndBlockedIPRanges = fmt.Errorf(
"cannot set both %s and %s annotations", consts.ServiceAnnotationAllowedIPRanges, consts.ServiceAnnotationBlockedIPRanges,
)
)

type AccessControl struct {
Expand All @@ -46,6 +50,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 +100,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 @@ -106,6 +118,11 @@ func NewAccessControl(logger logr.Logger, svc *v1.Service, sg *armnetwork.Securi
return nil, ErrSetBothLoadBalancerSourceRangesAndAllowedIPRanges
}

if len(allowedIPRanges) > 0 && len(blockedIPRanges) > 0 {
logger.Error(ErrSetBothAllowedAndBlockedIPRanges, "Forbidden configuration")
return nil, ErrSetBothAllowedAndBlockedIPRanges
}
Comment thread
nilo19 marked this conversation as resolved.
Outdated

if len(sourceRanges) > 0 && len(allowedServiceTags) > 0 {
logger.Info(
"Service is using both of spec.loadBalancerSourceRanges and annotation service.beta.kubernetes.io/azure-allowed-service-tags",
Expand All @@ -115,14 +132,17 @@ func NewAccessControl(logger logr.Logger, svc *v1.Service, sg *armnetwork.Securi
eventEmitter(svc, v1.EventTypeWarning, "ConflictConfiguration", EventMessageOfConflictLoadBalancerSourceRangesAndAllowedIPRanges())
}

invalidRanges := slices.Concat(invalidSourceRanges, invalidAllowedIPRanges, invalidBlockedIPRanges)
Comment thread
tomjankovec marked this conversation as resolved.
Outdated

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 +253,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
86 changes: 86 additions & 0 deletions pkg/provider/loadbalancer/accesscontrol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,35 @@ func TestNewAccessControl(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, called)
})

t.Run("it should return error if using both azure-allowed-ip-ranges and azure-blocked-ip-ranges", func(t *testing.T) {
svc := k8sFx.Service().
WithAllowedIPRanges("10.0.0.1/32").
WithBlockedIPRanges("20.0.0.1/32").
Build()

_, err := NewAccessControl(log.Noop(), &svc, sg)
assert.ErrorIs(t, err, ErrSetBothAllowedAndBlockedIPRanges)
})

t.Run("it should emit warning event if invalid azure-blocked-ip-ranges", func(t *testing.T) {
svc := k8sFx.Service().
WithBlockedIPRanges("foo", "10.0.0.1/32", "bar").
Build()

called := 0
eventEmitter := func(obj runtime.Object, eventType, reason, message string) {
called++
assert.Equal(t, &svc, obj)
assert.Equal(t, v1.EventTypeWarning, eventType)
assert.Equal(t, "InvalidBlockedIPRanges", reason)
assert.Equal(t, EventMessageOfInvalidBlockedIPRanges([]string{"foo", "bar"}), message)
}

_, err := NewAccessControl(log.Noop(), &svc, sg, WithEventEmitter(eventEmitter))
assert.NoError(t, err)
assert.Equal(t, 1, called)
})
}

func TestAccessControl_DenyAllExceptSourceRanges(t *testing.T) {
Expand Down Expand Up @@ -1134,6 +1163,63 @@ func TestAccessControl_PatchSecurityGroup(t *testing.T) {
)
runTest(t, svc, originalRules, dstIPv4Addresses, dstIPv6Addresses, true, expectedRules)
})

t.Run("patch service with blockedIPRanges only (deny before internet allow)", func(t *testing.T) {
var (
k8sFx = fixture.NewFixture().Kubernetes()
blockedIPv4Ranges = []string{"10.1.0.0/16"}
blockedIPv6Ranges = []string{"fd12:abcd::/48"}
svc = k8sFx.Service().
WithBlockedIPRanges(append(blockedIPv4Ranges, blockedIPv6Ranges...)...).
Build()
originalRules = azureFx.NoiseSecurityRules()
dstIPv4Addresses = []string{"52.0.0.1"}
dstIPv6Addresses = []string{"2001:db8::1"}
expectedRules = testutil.CloneInJSON(originalRules)
)
// Expected: deny blocked IPv4/IPv6 rules first, then internet allow rules
expectedRules = append(expectedRules,
// Deny rules for TCP
azureFx.DenyBlockedIPRangeSecurityRule(armnetwork.SecurityRuleProtocolTCP, iputil.IPv4, blockedIPv4Ranges, k8sFx.Service().TCPPorts()).WithPriority(400).WithDestination(dstIPv4Addresses...).Build(),
azureFx.DenyBlockedIPRangeSecurityRule(armnetwork.SecurityRuleProtocolTCP, iputil.IPv6, blockedIPv6Ranges, k8sFx.Service().TCPPorts()).WithPriority(401).WithDestination(dstIPv6Addresses...).Build(),
// Deny rules for UDP
azureFx.DenyBlockedIPRangeSecurityRule(armnetwork.SecurityRuleProtocolUDP, iputil.IPv4, blockedIPv4Ranges, k8sFx.Service().UDPPorts()).WithPriority(402).WithDestination(dstIPv4Addresses...).Build(),
azureFx.DenyBlockedIPRangeSecurityRule(armnetwork.SecurityRuleProtocolUDP, iputil.IPv6, blockedIPv6Ranges, k8sFx.Service().UDPPorts()).WithPriority(403).WithDestination(dstIPv6Addresses...).Build(),
// Allow rules
azureFx.AllowSecurityRule(armnetwork.SecurityRuleProtocolTCP, iputil.IPv4, []string{securitygroup.ServiceTagInternet}, k8sFx.Service().TCPPorts()).WithPriority(500).WithDestination(dstIPv4Addresses...).Build(),
azureFx.AllowSecurityRule(armnetwork.SecurityRuleProtocolTCP, iputil.IPv6, []string{securitygroup.ServiceTagInternet}, k8sFx.Service().TCPPorts()).WithPriority(501).WithDestination(dstIPv6Addresses...).Build(),
azureFx.AllowSecurityRule(armnetwork.SecurityRuleProtocolUDP, iputil.IPv4, []string{securitygroup.ServiceTagInternet}, k8sFx.Service().UDPPorts()).WithPriority(502).WithDestination(dstIPv4Addresses...).Build(),
azureFx.AllowSecurityRule(armnetwork.SecurityRuleProtocolUDP, iputil.IPv6, []string{securitygroup.ServiceTagInternet}, k8sFx.Service().UDPPorts()).WithPriority(503).WithDestination(dstIPv6Addresses...).Build(),
)
runTest(t, svc, originalRules, dstIPv4Addresses, dstIPv6Addresses, true, expectedRules)
})

t.Run("patch service with blockedIPRanges and allowedServiceTags", func(t *testing.T) {
Comment thread
tomjankovec marked this conversation as resolved.
var (
k8sFx = fixture.NewFixture().Kubernetes()
serviceTags = azureFx.ServiceTags(1)
blockedIPv4Ranges = []string{"10.1.0.0/16"}
svc = k8sFx.Service().
WithBlockedIPRanges(blockedIPv4Ranges...).
WithAllowedServiceTags(serviceTags...).
Build()
originalRules = azureFx.NoiseSecurityRules()
dstIPv4Addresses = []string{"52.0.0.1"}
dstIPv6Addresses = []string{}
expectedRules = testutil.CloneInJSON(originalRules)
)
// Expected: deny blocked IPv4 rule then service tag allow (no internet tag since service tag specified)
expectedRules = append(expectedRules,
// Deny rules for TCP
azureFx.DenyBlockedIPRangeSecurityRule(armnetwork.SecurityRuleProtocolTCP, iputil.IPv4, blockedIPv4Ranges, k8sFx.Service().TCPPorts()).WithPriority(400).WithDestination(dstIPv4Addresses...).Build(),
// Deny rules for UDP
azureFx.DenyBlockedIPRangeSecurityRule(armnetwork.SecurityRuleProtocolUDP, iputil.IPv4, blockedIPv4Ranges, k8sFx.Service().UDPPorts()).WithPriority(401).WithDestination(dstIPv4Addresses...).Build(),
// Allow rules
azureFx.AllowSecurityRule(armnetwork.SecurityRuleProtocolTCP, iputil.IPv4, []string{serviceTags[0]}, k8sFx.Service().TCPPorts()).WithPriority(500).WithDestination(dstIPv4Addresses...).Build(),
azureFx.AllowSecurityRule(armnetwork.SecurityRuleProtocolUDP, iputil.IPv4, []string{serviceTags[0]}, k8sFx.Service().UDPPorts()).WithPriority(501).WithDestination(dstIPv4Addresses...).Build(),
)
runTest(t, svc, originalRules, dstIPv4Addresses, dstIPv6Addresses, true, expectedRules)
})
}

func TestAccessControl_RetainSecurityGroup(t *testing.T) {
Expand Down
Loading
Loading