Skip to content

Commit c7cbffb

Browse files
committed
feat: Introduce annotation for setting explicit egress CIDR ranges on Service LB frontend security group
1 parent 7cd6d46 commit c7cbffb

13 files changed

+625
-7
lines changed

docs/guide/service/annotations.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
| [service.beta.kubernetes.io/aws-load-balancer-minimum-load-balancer-capacity](#load-balancer-capacity-reservation) | stringMap | |
6666
| [service.beta.kubernetes.io/aws-load-balancer-enable-icmp-for-path-mtu-discovery](#icmp-path-mtu-discovery) | string | | If specified, a security group rule is added to the managed security group to allow explicit ICMP traffic for [Path MTU discovery](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/network_mtu.html#path_mtu_discovery) for IPv4 and dual-stack VPCs. Creates a rule for each source range if `service.beta.kubernetes.io/load-balancer-source-ranges` is present. |
6767
| [service.beta.kubernetes.io/aws-load-balancer-enable-tcp-udp-listener](#tcp-udp-listener) | boolean | false | If specified, the controller will attempt to try TCP_UDP Listeners when the service defines a TCP and UDP port on the same port number. |
68-
| [service.beta.kubernetes.io/aws-load-balancer-disable-nlb-sg](#nlb-sg-disable) | boolean | false | If specified, the controller will not create or manage Security Groups for the service. |
68+
| [service.beta.kubernetes.io/aws-load-balancer-disable-nlb-sg](#nlb-sg-disable) | boolean | false | If specified, the controller will not create or manage Security Groups for the service. |
69+
| [service.beta.kubernetes.io/aws-load-balancer-outbound-cidrs](#outbound-cidrs) | stringList | | If specified, the controller will add the CIDR ranges as egress rules to the managed frontend security group, instead of relying on the default AWS `0.0.0.0/0` egress rule. If not set, aws-load-balancer-controller will maintain previous behavior and not manage egress rules at all. |
6970

7071
## Traffic Routing
7172
Traffic Routing can be controlled with following annotations:
@@ -354,7 +355,6 @@ for proxy protocol v2 configuration.
354355
service.beta.kubernetes.io/aws-load-balancer-disable-nlb-sg: "true"
355356
```
356357
357-
358358
- <a name="deprecated-attributes"></a>the following annotations are deprecated in v2.3.0 release in favor of [service.beta.kubernetes.io/aws-load-balancer-attributes](#load-balancer-attributes)
359359
360360
!!!note ""
@@ -621,6 +621,21 @@ Load balancer access can be controlled via following annotations:
621621
service.beta.kubernetes.io/aws-load-balancer-inbound-sg-rules-on-private-link-traffic: "off"
622622
```
623623
624+
- <a name="outbound-cidrs">`service.beta.kubernetes.io/aws-load-balancer-outbound-cidrs`</a> allows specifying a comma-delimited list of CIDR ranges to be added as egress rules to the frontend security group.
625+
626+
!!!note ""
627+
- Historically, `aws-load-balancer-controller` hasn't explicitly added any egress rules to managed frontend security groups - instead, it relies on the fact that AWS will add a default `0.0.0.0/0` outbound egress rule for all SGs created without an explicit egress rule list. This is required for the load balancer to be able to talk to the target group and potentially other services (e.g. CloudWatch).
628+
629+
- However, some organizations may have issues with the default `0.0.0.0/0` egress rule (e.g. security scanners may flag them) and would rather be able to further limit the rule to a specific set of CIDR range(s). This annotation allows that.
630+
631+
!!!warning "Note"
632+
- If this annotation is not present, `aws-load-balancer-controller` will effectively not manage egress rules at all, maintaining the behavior before the annotation was added. This means that if the annotation is added to a service to set the egress security group rules and then subsequently removed, the egress security group rule will not be removed automatically.
633+
634+
!!!example
635+
```
636+
service.beta.kubernetes.io/aws-load-balancer-outbound-cidrs: "172.18.0.0/16"
637+
```
638+
624639
## Capacity Unit Reservation
625640
Load balancer capacity unit reservation can be configured via following annotations:
626641

pkg/annotations/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,5 @@ const (
124124
SvcLBSuffixEnableIcmpForPathMtuDiscovery = "aws-load-balancer-enable-icmp-for-path-mtu-discovery"
125125
SvcLBSuffixEnableTCPUDPListener = "aws-load-balancer-enable-tcp-udp-listener"
126126
SvcLBSuffixDisableNLBSG = "aws-load-balancer-disable-nlb-sg"
127+
SvcLBSuffixOutboundCIDRs = "aws-load-balancer-outbound-cidrs"
127128
)

pkg/aws/services/ec2.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ type EC2 interface {
3333
CreateSecurityGroupWithContext(ctx context.Context, input *ec2.CreateSecurityGroupInput) (*ec2.CreateSecurityGroupOutput, error)
3434
DeleteSecurityGroupWithContext(ctx context.Context, input *ec2.DeleteSecurityGroupInput) (*ec2.DeleteSecurityGroupOutput, error)
3535
AuthorizeSecurityGroupIngressWithContext(ctx context.Context, input *ec2.AuthorizeSecurityGroupIngressInput) (*ec2.AuthorizeSecurityGroupIngressOutput, error)
36+
AuthorizeSecurityGroupEgressWithContext(ctx context.Context, input *ec2.AuthorizeSecurityGroupEgressInput) (*ec2.AuthorizeSecurityGroupEgressOutput, error)
3637
RevokeSecurityGroupIngressWithContext(ctx context.Context, input *ec2.RevokeSecurityGroupIngressInput) (*ec2.RevokeSecurityGroupIngressOutput, error)
38+
RevokeSecurityGroupEgressWithContext(ctx context.Context, input *ec2.RevokeSecurityGroupEgressInput) (*ec2.RevokeSecurityGroupEgressOutput, error)
3739
DescribeAvailabilityZonesWithContext(ctx context.Context, input *ec2.DescribeAvailabilityZonesInput) (*ec2.DescribeAvailabilityZonesOutput, error)
3840
DescribeVpcsWithContext(ctx context.Context, input *ec2.DescribeVpcsInput) (*ec2.DescribeVpcsOutput, error)
3941
DescribeInstancesWithContext(ctx context.Context, input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error)
@@ -202,6 +204,14 @@ func (c *ec2Client) AuthorizeSecurityGroupIngressWithContext(ctx context.Context
202204
return client.AuthorizeSecurityGroupIngress(ctx, input)
203205
}
204206

207+
func (c *ec2Client) AuthorizeSecurityGroupEgressWithContext(ctx context.Context, input *ec2.AuthorizeSecurityGroupEgressInput) (*ec2.AuthorizeSecurityGroupEgressOutput, error) {
208+
client, err := c.awsClientsProvider.GetEC2Client(ctx, "AuthorizeSecurityGroupIngress")
209+
if err != nil {
210+
return nil, err
211+
}
212+
return client.AuthorizeSecurityGroupEgress(ctx, input)
213+
}
214+
205215
func (c *ec2Client) RevokeSecurityGroupIngressWithContext(ctx context.Context, input *ec2.RevokeSecurityGroupIngressInput) (*ec2.RevokeSecurityGroupIngressOutput, error) {
206216
client, err := c.awsClientsProvider.GetEC2Client(ctx, "RevokeSecurityGroupIngress")
207217
if err != nil {
@@ -210,6 +220,14 @@ func (c *ec2Client) RevokeSecurityGroupIngressWithContext(ctx context.Context, i
210220
return client.RevokeSecurityGroupIngress(ctx, input)
211221
}
212222

223+
func (c *ec2Client) RevokeSecurityGroupEgressWithContext(ctx context.Context, input *ec2.RevokeSecurityGroupEgressInput) (*ec2.RevokeSecurityGroupEgressOutput, error) {
224+
client, err := c.awsClientsProvider.GetEC2Client(ctx, "RevokeSecurityGroupEgress")
225+
if err != nil {
226+
return nil, err
227+
}
228+
return client.RevokeSecurityGroupEgress(ctx, input)
229+
}
230+
213231
func (c *ec2Client) DescribeAvailabilityZonesWithContext(ctx context.Context, input *ec2.DescribeAvailabilityZonesInput) (*ec2.DescribeAvailabilityZonesOutput, error) {
214232
client, err := c.awsClientsProvider.GetEC2Client(ctx, "DescribeAvailabilityZones")
215233
if err != nil {

pkg/aws/services/ec2_mocks.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/deploy/ec2/security_group_manager.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,50 @@ func (m *defaultSecurityGroupManager) Create(ctx context.Context, resSG *ec2mode
9898
return ec2model.SecurityGroupStatus{}, err
9999
}
100100

101+
// Only reconcile egress rules when explicitly set by the service.beta.kubernetes.io/aws-load-balancer-outbound-cidrs annotation. Otherwise, preserve the previous behavior of not touching egress rules.
102+
if resSG.Spec.Egress != nil {
103+
permissionInfosEgress, err := buildIPPermissionInfos(resSG.Spec.Egress)
104+
if err != nil {
105+
return ec2model.SecurityGroupStatus{}, err
106+
}
107+
108+
if err := m.networkingSGReconciler.ReconcileEgress(ctx, sgID, permissionInfosEgress); err != nil {
109+
return ec2model.SecurityGroupStatus{}, err
110+
}
111+
}
112+
101113
return ec2model.SecurityGroupStatus{
102114
GroupID: sgID,
103115
}, nil
104116
}
105117

106118
func (m *defaultSecurityGroupManager) Update(ctx context.Context, resSG *ec2model.SecurityGroup, sdkSG networking.SecurityGroupInfo) (ec2model.SecurityGroupStatus, error) {
107119
permissionInfos, err := buildIPPermissionInfos(resSG.Spec.Ingress)
120+
108121
if err != nil {
109122
return ec2model.SecurityGroupStatus{}, err
110123
}
124+
111125
if err := m.updateSDKSecurityGroupGroupWithTags(ctx, resSG, sdkSG); err != nil {
112126
return ec2model.SecurityGroupStatus{}, err
113127
}
114128
if err := m.networkingSGReconciler.ReconcileIngress(ctx, sdkSG.SecurityGroupID, permissionInfos); err != nil {
115129
return ec2model.SecurityGroupStatus{}, err
116130
}
131+
132+
// Only reconcile egress rules when explicitly set by the service.beta.kubernetes.io/aws-load-balancer-outbound-cidrs annotation. Otherwise, preserve the previous behavior of not touching egress rules.
133+
if resSG.Spec.Egress != nil {
134+
permissionInfosEgress, err := buildIPPermissionInfos(resSG.Spec.Egress)
135+
136+
if err != nil {
137+
return ec2model.SecurityGroupStatus{}, err
138+
}
139+
140+
if err := m.networkingSGReconciler.ReconcileEgress(ctx, sdkSG.SecurityGroupID, permissionInfosEgress); err != nil {
141+
return ec2model.SecurityGroupStatus{}, err
142+
}
143+
}
144+
117145
return ec2model.SecurityGroupStatus{
118146
GroupID: sdkSG.SecurityGroupID,
119147
}, nil

pkg/model/ec2/security_group.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ type SecurityGroupSpec struct {
6161

6262
// +optional
6363
Ingress []IPPermission `json:"ingress,omitempty"`
64+
65+
// +optional
66+
Egress []IPPermission `json:"egress,omitempty"`
6467
}
6568

6669
// SecurityGroupStatus defines the observed state of SecurityGroup

pkg/networking/networking_manager_test.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,8 +2069,8 @@ func Test_AttemptGarbageCollection(t *testing.T) {
20692069
assert.Error(t, err)
20702070
} else {
20712071
assert.NoError(t, err)
2072-
assert.Equal(t, len(tt.expectedSgReconciles), len(mockReconciler.calls))
2073-
for _, call := range mockReconciler.calls {
2072+
assert.Equal(t, len(tt.expectedSgReconciles), len(mockReconciler.ingressCalls))
2073+
for _, call := range mockReconciler.ingressCalls {
20742074
assert.True(t, tt.expectedSgReconciles.Has(call.sgID), fmt.Sprintf("expected sgID: %s to be in calls", call.sgID))
20752075
}
20762076
}
@@ -2083,12 +2083,29 @@ type reconcileIngressCall struct {
20832083
desiredPermissions []IPPermissionInfo
20842084
opts []SecurityGroupReconcileOption
20852085
}
2086+
2087+
type reconcileEgressCall struct {
2088+
sgID string
2089+
desiredPermissions []IPPermissionInfo
2090+
opts []SecurityGroupReconcileOption
2091+
}
2092+
20862093
type mockSGReconciler struct {
2087-
calls []reconcileIngressCall
2094+
ingressCalls []reconcileIngressCall
2095+
egressCalls []reconcileEgressCall
20882096
}
20892097

20902098
func (m *mockSGReconciler) ReconcileIngress(ctx context.Context, sgID string, desiredPermissions []IPPermissionInfo, opts ...SecurityGroupReconcileOption) error {
2091-
m.calls = append(m.calls, reconcileIngressCall{
2099+
m.ingressCalls = append(m.ingressCalls, reconcileIngressCall{
2100+
sgID: sgID,
2101+
desiredPermissions: desiredPermissions,
2102+
opts: opts,
2103+
})
2104+
return nil
2105+
}
2106+
2107+
func (m *mockSGReconciler) ReconcileEgress(ctx context.Context, sgID string, desiredPermissions []IPPermissionInfo, opts ...SecurityGroupReconcileOption) error {
2108+
m.egressCalls = append(m.egressCalls, reconcileEgressCall{
20922109
sgID: sgID,
20932110
desiredPermissions: desiredPermissions,
20942111
opts: opts,

pkg/networking/security_group_info.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ type SecurityGroupInfo struct {
2323
// Ingress permission for securityGroup.
2424
Ingress []IPPermissionInfo
2525

26+
// Egress permission for securityGroup
27+
Egress []IPPermissionInfo
28+
2629
// Tags for securityGroup.
2730
Tags map[string]string
2831
}
@@ -74,10 +77,17 @@ func NewRawSecurityGroupInfo(sdkSG ec2types.SecurityGroup) SecurityGroupInfo {
7477
ingress = append(ingress, NewRawIPPermission(expandedPermission))
7578
}
7679
}
80+
var egress []IPPermissionInfo
81+
for _, sdkPermission := range sdkSG.IpPermissionsEgress {
82+
for _, expandedPermission := range expandSDKIPPermission(sdkPermission) {
83+
egress = append(egress, NewRawIPPermission(expandedPermission))
84+
}
85+
}
7786
tags := buildSecurityGroupTags(sdkSG)
7887
return SecurityGroupInfo{
7988
SecurityGroupID: sgID,
8089
Ingress: ingress,
90+
Egress: egress,
8191
Tags: tags,
8292
}
8393
}

pkg/networking/security_group_manager.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ type SecurityGroupManager interface {
5151
// AuthorizeSGIngress will authorize Ingress permissions to SecurityGroup.
5252
AuthorizeSGIngress(ctx context.Context, sgID string, permissions []IPPermissionInfo) error
5353

54+
// AuthorizeSGEgress will authorize Ingress permissions to SecurityGroup.
55+
AuthorizeSGEgress(ctx context.Context, sgID string, permissions []IPPermissionInfo) error
56+
5457
// RevokeSGIngress will revoke Ingress permissions from SecurityGroup.
5558
RevokeSGIngress(ctx context.Context, sgID string, permissions []IPPermissionInfo) error
59+
60+
// RevokeSGEgress will revoke Ingress permissions from SecurityGroup.
61+
RevokeSGEgress(ctx context.Context, sgID string, permissions []IPPermissionInfo) error
5662
}
5763

5864
// NewDefaultSecurityGroupManager constructs new defaultSecurityGroupManager.
@@ -146,6 +152,26 @@ func (m *defaultSecurityGroupManager) AuthorizeSGIngress(ctx context.Context, sg
146152
return nil
147153
}
148154

155+
func (m *defaultSecurityGroupManager) AuthorizeSGEgress(ctx context.Context, sgID string, permissions []IPPermissionInfo) error {
156+
sdkIPPermissions := buildSDKIPPermissions(permissions)
157+
req := &ec2sdk.AuthorizeSecurityGroupEgressInput{
158+
GroupId: awssdk.String(sgID),
159+
IpPermissions: sdkIPPermissions,
160+
}
161+
m.logger.Info("authorizing securityGroup egress",
162+
"securityGroupID", sgID,
163+
"permission", sdkIPPermissions)
164+
if _, err := m.ec2Client.AuthorizeSecurityGroupEgressWithContext(ctx, req); err != nil {
165+
return err
166+
}
167+
m.logger.Info("authorized securityGroup egress",
168+
"securityGroupID", sgID)
169+
170+
// TODO: ideally we can remember the permissions we granted to save DescribeSecurityGroup API calls.
171+
m.clearSGInfosFromCache(sgID)
172+
return nil
173+
}
174+
149175
func (m *defaultSecurityGroupManager) RevokeSGIngress(ctx context.Context, sgID string, permissions []IPPermissionInfo) error {
150176
sdkIPPermissions := buildSDKIPPermissions(permissions)
151177
req := &ec2sdk.RevokeSecurityGroupIngressInput{
@@ -166,6 +192,26 @@ func (m *defaultSecurityGroupManager) RevokeSGIngress(ctx context.Context, sgID
166192
return nil
167193
}
168194

195+
func (m *defaultSecurityGroupManager) RevokeSGEgress(ctx context.Context, sgID string, permissions []IPPermissionInfo) error {
196+
sdkIPPermissions := buildSDKIPPermissions(permissions)
197+
req := &ec2sdk.RevokeSecurityGroupEgressInput{
198+
GroupId: awssdk.String(sgID),
199+
IpPermissions: sdkIPPermissions,
200+
}
201+
m.logger.Info("revoking securityGroup egress",
202+
"securityGroupID", sgID,
203+
"permission", sdkIPPermissions)
204+
if _, err := m.ec2Client.RevokeSecurityGroupEgressWithContext(ctx, req); err != nil {
205+
return err
206+
}
207+
m.logger.Info("revoked securityGroup egress",
208+
"securityGroupID", sgID)
209+
210+
// TODO: ideally we can remember the permissions we revoked to save DescribeSecurityGroup API calls.
211+
m.clearSGInfosFromCache(sgID)
212+
return nil
213+
}
214+
169215
func (m *defaultSecurityGroupManager) fetchSGInfosFromCache(sgIDs []string) map[string]SecurityGroupInfo {
170216
m.sgInfoCacheMutex.RLock()
171217
defer m.sgInfoCacheMutex.RUnlock()

0 commit comments

Comments
 (0)