Skip to content

Commit 34b43e6

Browse files
committed
Add support for enabling AWS Shield Advanced protection
Shield Advanced improvements - Add caching of AWS API requests - Add feature gate - When deleting protection, check protection name to match one indicating it was created by alb-ingress-controller alb-ingress-controller - Changed annotation semantics of shield-advanced-protection to: - 'true': enables protection - 'false': disables protection created by alb-ingress-controller - missing annoation: immediately returns from reconcile skipping all Shield related API calls
1 parent 70df39b commit 34b43e6

File tree

15 files changed

+2234
-6
lines changed

15 files changed

+2234
-6
lines changed

docs/examples/iam-policy.json

+12
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@
113113
"waf:GetWebACL"
114114
],
115115
"Resource": "*"
116+
},
117+
{
118+
"Effect": "Allow",
119+
"Action": [
120+
"shield:DescribeProtection",
121+
"shield:GetSubscriptionState",
122+
"shield:DeleteProtection",
123+
"shield:CreateProtection",
124+
"shield:DescribeSubscription",
125+
"shield:ListProtections"
126+
],
127+
"Resource": "*"
116128
}
117129
]
118130
}

docs/guide/ingress/annotation.md

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ You can add kubernetes annotations to ingress and service objects to customize t
44
!!!note
55
- Annotations applied to service have higher priority over annotations applied to ingress. `Location` column below indicates where that annotation can be applied to.
66
- Annotation keys and values can only be strings. Advanced format are encoded as below:
7+
- boolean: 'true'
78
- integer: '42'
89
- stringMap: k1=v1,k2=v2
910
- stringList: s1,s2,s3
@@ -37,6 +38,7 @@ You can add kubernetes annotations to ingress and service objects to customize t
3738
|[alb.ingress.kubernetes.io/load-balancer-attributes](#load-balancer-attributes)|stringMap|N/A|ingress|
3839
|[alb.ingress.kubernetes.io/scheme](#scheme)|internal \| internet-facing|internal|ingress|
3940
|[alb.ingress.kubernetes.io/security-groups](#security-groups)|stringList|N/A|ingress|
41+
|[alb.ingress.kubernetes.io/shield-advanced-protection](#shield-advanced-protection)|boolean|N/A|ingress|
4042
|[alb.ingress.kubernetes.io/ssl-policy](#ssl-policy)|string|ELBSecurityPolicy-2016-08|ingress|
4143
|[alb.ingress.kubernetes.io/subnets](#subnets)|stringList|N/A|ingress|
4244
|[alb.ingress.kubernetes.io/success-codes](#success-codes)|string|'200'|ingress,service|
@@ -488,6 +490,13 @@ Health check on target groups can be controlled with following annotations:
488490
```alb.ingress.kubernetes.io/waf-acl-id: 499e8b99-6671-4614-a86d-adb1810b7fbe
489491
```
490492

493+
## Shield Advanced
494+
- <a name="shield-advanced-protection">`alb.ingress.kubernetes.io/shield-advanced-protection`</a> turns on / off the AWS Shield Advanced protection for the load balancer.
495+
496+
!!!example
497+
```alb.ingress.kubernetes.io/shield-advanced-protection: 'true'
498+
```
499+
491500
## SSL
492501
SSL support can be controlled with following annotations:
493502

internal/alb/lb/loadbalancer.go

+9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func NewController(
4646
tagsController tags.Controller) Controller {
4747
attrsController := NewAttributesController(cloud)
4848
wafController := NewWAFController(cloud)
49+
shieldController := NewShieldController(cloud)
4950

5051
return &defaultController{
5152
cloud: cloud,
@@ -57,6 +58,7 @@ func NewController(
5758
tagsController: tagsController,
5859
attrsController: attrsController,
5960
wafController: wafController,
61+
shieldController: shieldController,
6062
}
6163
}
6264

@@ -81,6 +83,7 @@ type defaultController struct {
8183
tagsController tags.Controller
8284
attrsController AttributesController
8385
wafController WAFController
86+
shieldController ShieldController
8487
}
8588

8689
var _ Controller = (*defaultController)(nil)
@@ -119,6 +122,12 @@ func (controller *defaultController) Reconcile(ctx context.Context, ingress *ext
119122
}
120123
}
121124

125+
if controller.store.GetConfig().FeatureGate.Enabled(config.ShieldAdvanced) {
126+
if err := controller.shieldController.Reconcile(ctx, lbArn, ingress); err != nil {
127+
return nil, err
128+
}
129+
}
130+
122131
tgGroup, err := controller.tgGroupController.Reconcile(ctx, ingress)
123132
if err != nil {
124133
return nil, fmt.Errorf("failed to reconcile targetGroups due to %v", err)

internal/alb/lb/shield.go

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package lb
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"time"
8+
9+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/ingress/annotations"
10+
11+
"github.com/aws/aws-sdk-go/service/shield"
12+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/albctx"
13+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws"
14+
"github.com/pkg/errors"
15+
extensions "k8s.io/api/extensions/v1beta1"
16+
17+
"k8s.io/apimachinery/pkg/util/cache"
18+
)
19+
20+
const (
21+
protectionEnabledForLBCacheMaxSize = 1024
22+
protectionEnabledForLBCacheTTL = 10 * time.Second
23+
shieldAvailableCacheMaxSize = 1
24+
shieldAvailableCacheTTL = 10 * time.Second
25+
shieldAvailableCacheKey = "shieldAvailable"
26+
protectionName = "managed by aws-alb-ingress-controller"
27+
)
28+
29+
// ShieldController provides functionality to manage ALB's Shield Advanced protection.
30+
type ShieldController interface {
31+
Reconcile(ctx context.Context, lbArn string, ingress *extensions.Ingress) error
32+
}
33+
34+
func NewShieldController(cloud aws.CloudAPI) ShieldController {
35+
return &defaultShieldController{
36+
cloud: cloud,
37+
protectionEnabledForLBCache: cache.NewLRUExpireCache(protectionEnabledForLBCacheMaxSize),
38+
shieldAvailableCache: cache.NewLRUExpireCache(shieldAvailableCacheMaxSize),
39+
}
40+
}
41+
42+
type defaultShieldController struct {
43+
cloud aws.CloudAPI
44+
45+
// cache that stores protection id for LoadBalancerARN.
46+
// The cache value is string, while "" represents no protection.
47+
protectionEnabledForLBCache *cache.LRUExpireCache
48+
// cache that stores shield availability for current account.
49+
// The cache value is string, while "" represents not available.
50+
shieldAvailableCache *cache.LRUExpireCache
51+
}
52+
53+
func (c *defaultShieldController) Reconcile(ctx context.Context, lbArn string, ingress *extensions.Ingress) error {
54+
var enableProtection bool
55+
annotationPresent, err := annotations.LoadBoolAnnocation("shield-advanced-protection", &enableProtection, ingress.Annotations)
56+
if err != nil {
57+
return err
58+
}
59+
if !annotationPresent {
60+
return nil
61+
}
62+
63+
available, err := c.getCurrentShieldAvailability(ctx)
64+
if err != nil {
65+
return err
66+
}
67+
68+
if enableProtection && !available {
69+
return fmt.Errorf("unable to create shield advanced protection for loadBalancer %v, shield advanced subscription is not active", lbArn)
70+
}
71+
72+
protection, err := c.getCurrentProtectionStatus(ctx, lbArn)
73+
if err != nil {
74+
return fmt.Errorf("failed to get shield advanced protection status for loadBalancer %v due to %v", lbArn, err)
75+
}
76+
77+
if protection == nil && enableProtection {
78+
_, err := c.cloud.CreateProtection(ctx, aws.String(lbArn), aws.String(protectionName))
79+
if err != nil {
80+
return fmt.Errorf("failed to create shield advanced protection for loadBalancer %v due to %v", lbArn, err)
81+
}
82+
albctx.GetLogger(ctx).Infof("enabled shield advanced protection for %v", lbArn)
83+
} else if protection != nil && !enableProtection {
84+
if aws.StringValue(protection.Name) == protectionName {
85+
_, err := c.cloud.DeleteProtection(ctx, protection.Id)
86+
c.protectionEnabledForLBCache.Remove(lbArn)
87+
if err != nil {
88+
return fmt.Errorf("failed to delete shield advanced protection for loadBalancer %v due to %v", lbArn, err)
89+
}
90+
91+
albctx.GetLogger(ctx).Infof("deleted shield advanced protection for %v", lbArn)
92+
} else {
93+
albctx.GetLogger(ctx).Warnf("unable to delete shield advanced protection for %v, the protection name does not match \"%v\"", lbArn, protectionName)
94+
}
95+
}
96+
return nil
97+
}
98+
99+
func (c *defaultShieldController) getCurrentShieldAvailability(ctx context.Context) (bool, error) {
100+
cachedShieldAvailable, exists := c.shieldAvailableCache.Get(shieldAvailableCacheKey)
101+
if exists {
102+
available, err := strconv.ParseBool(cachedShieldAvailable.(string))
103+
if err == nil {
104+
return available, nil
105+
}
106+
}
107+
108+
available, err := c.cloud.ShieldAvailable(ctx)
109+
if err != nil {
110+
return false, fmt.Errorf("failed to get shield advanced subscription state %v", err)
111+
}
112+
c.shieldAvailableCache.Add(shieldAvailableCacheKey, "true", shieldAvailableCacheTTL)
113+
return available, nil
114+
}
115+
116+
func (c *defaultShieldController) getCurrentProtectionStatus(ctx context.Context, lbArn string) (*shield.Protection, error) {
117+
cachedProtection, exists := c.protectionEnabledForLBCache.Get(lbArn)
118+
if exists {
119+
return cachedProtection.(*shield.Protection), nil
120+
}
121+
122+
protection, err := c.cloud.GetProtection(ctx, aws.String(lbArn))
123+
if err != nil {
124+
return nil, errors.Wrapf(err, "failed to get protection status for load balancer %v", lbArn)
125+
}
126+
127+
c.protectionEnabledForLBCache.Add(lbArn, protection, protectionEnabledForLBCacheTTL)
128+
return protection, nil
129+
}

0 commit comments

Comments
 (0)