Skip to content

Add support for enabling AWS Shield Advanced protection #1126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 7, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions docs/examples/iam-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@
"waf:GetWebACL"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"shield:DescribeProtection",
"shield:GetSubscriptionState",
"shield:DeleteProtection",
"shield:CreateProtection",
"shield:DescribeSubscription",
"shield:ListProtections"
],
"Resource": "*"
}
]
}
9 changes: 9 additions & 0 deletions docs/guide/ingress/annotation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ You can add kubernetes annotations to ingress and service objects to customize t
!!!note
- Annotations applied to service have higher priority over annotations applied to ingress. `Location` column below indicates where that annotation can be applied to.
- Annotation keys and values can only be strings. Advanced format are encoded as below:
- boolean: 'true'
- integer: '42'
- stringMap: k1=v1,k2=v2
- stringList: s1,s2,s3
Expand Down Expand Up @@ -37,6 +38,7 @@ You can add kubernetes annotations to ingress and service objects to customize t
|[alb.ingress.kubernetes.io/load-balancer-attributes](#load-balancer-attributes)|stringMap|N/A|ingress|
|[alb.ingress.kubernetes.io/scheme](#scheme)|internal \| internet-facing|internal|ingress|
|[alb.ingress.kubernetes.io/security-groups](#security-groups)|stringList|N/A|ingress|
|[alb.ingress.kubernetes.io/shield-advanced-protection](#shield-advanced-protection)|boolean|N/A|ingress|
|[alb.ingress.kubernetes.io/ssl-policy](#ssl-policy)|string|ELBSecurityPolicy-2016-08|ingress|
|[alb.ingress.kubernetes.io/subnets](#subnets)|stringList|N/A|ingress|
|[alb.ingress.kubernetes.io/success-codes](#success-codes)|string|'200'|ingress,service|
Expand Down Expand Up @@ -488,6 +490,13 @@ Health check on target groups can be controlled with following annotations:
```alb.ingress.kubernetes.io/waf-acl-id: 499e8b99-6671-4614-a86d-adb1810b7fbe
```

## Shield Advanced
- <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.

!!!example
```alb.ingress.kubernetes.io/shield-advanced-protection: 'true'
```

## SSL
SSL support can be controlled with following annotations:

Expand Down
9 changes: 9 additions & 0 deletions internal/alb/lb/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func NewController(
tagsController tags.Controller) Controller {
attrsController := NewAttributesController(cloud)
wafController := NewWAFController(cloud)
shieldController := NewShieldController(cloud)

return &defaultController{
cloud: cloud,
Expand All @@ -57,6 +58,7 @@ func NewController(
tagsController: tagsController,
attrsController: attrsController,
wafController: wafController,
shieldController: shieldController,
}
}

Expand All @@ -81,6 +83,7 @@ type defaultController struct {
tagsController tags.Controller
attrsController AttributesController
wafController WAFController
shieldController ShieldController
}

var _ Controller = (*defaultController)(nil)
Expand Down Expand Up @@ -119,6 +122,12 @@ func (controller *defaultController) Reconcile(ctx context.Context, ingress *ext
}
}

if controller.store.GetConfig().FeatureGate.Enabled(config.ShieldAdvanced) {
if err := controller.shieldController.Reconcile(ctx, lbArn, ingress); err != nil {
return nil, err
}
}

tgGroup, err := controller.tgGroupController.Reconcile(ctx, ingress)
if err != nil {
return nil, fmt.Errorf("failed to reconcile targetGroups due to %v", err)
Expand Down
129 changes: 129 additions & 0 deletions internal/alb/lb/shield.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package lb

import (
"context"
"fmt"
"strconv"
"time"

"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/ingress/annotations"

"github.com/aws/aws-sdk-go/service/shield"
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/albctx"
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws"
"github.com/pkg/errors"
extensions "k8s.io/api/extensions/v1beta1"

"k8s.io/apimachinery/pkg/util/cache"
)

const (
protectionEnabledForLBCacheMaxSize = 1024
protectionEnabledForLBCacheTTL = 10 * time.Second
shieldAvailableCacheMaxSize = 1
shieldAvailableCacheTTL = 10 * time.Second
shieldAvailableCacheKey = "shieldAvailable"
protectionName = "managed by aws-alb-ingress-controller"
)

// ShieldController provides functionality to manage ALB's Shield Advanced protection.
type ShieldController interface {
Reconcile(ctx context.Context, lbArn string, ingress *extensions.Ingress) error
}

func NewShieldController(cloud aws.CloudAPI) ShieldController {
return &defaultShieldController{
cloud: cloud,
protectionEnabledForLBCache: cache.NewLRUExpireCache(protectionEnabledForLBCacheMaxSize),
shieldAvailableCache: cache.NewLRUExpireCache(shieldAvailableCacheMaxSize),
}
}

type defaultShieldController struct {
cloud aws.CloudAPI

// cache that stores protection id for LoadBalancerARN.
// The cache value is string, while "" represents no protection.
protectionEnabledForLBCache *cache.LRUExpireCache
// cache that stores shield availability for current account.
// The cache value is string, while "" represents not available.
shieldAvailableCache *cache.LRUExpireCache
}

func (c *defaultShieldController) Reconcile(ctx context.Context, lbArn string, ingress *extensions.Ingress) error {
var enableProtection bool
annotationPresent, err := annotations.LoadBoolAnnocation("shield-advanced-protection", &enableProtection, ingress.Annotations)
if err != nil {
return err
}
if !annotationPresent {
return nil
}

available, err := c.getCurrentShieldAvailability(ctx)
if err != nil {
return err
}

if enableProtection && !available {
return fmt.Errorf("unable to create shield advanced protection for loadBalancer %v, shield advanced subscription is not active", lbArn)
}

protection, err := c.getCurrentProtectionStatus(ctx, lbArn)
if err != nil {
return fmt.Errorf("failed to get shield advanced protection status for loadBalancer %v due to %v", lbArn, err)
}

if protection == nil && enableProtection {
_, err := c.cloud.CreateProtection(ctx, aws.String(lbArn), aws.String(protectionName))
if err != nil {
return fmt.Errorf("failed to create shield advanced protection for loadBalancer %v due to %v", lbArn, err)
}
albctx.GetLogger(ctx).Infof("enabled shield advanced protection for %v", lbArn)
} else if protection != nil && !enableProtection {
if aws.StringValue(protection.Name) == protectionName {
_, err := c.cloud.DeleteProtection(ctx, protection.Id)
c.protectionEnabledForLBCache.Remove(lbArn)
if err != nil {
return fmt.Errorf("failed to delete shield advanced protection for loadBalancer %v due to %v", lbArn, err)
}

albctx.GetLogger(ctx).Infof("deleted shield advanced protection for %v", lbArn)
} else {
albctx.GetLogger(ctx).Warnf("unable to delete shield advanced protection for %v, the protection name does not match \"%v\"", lbArn, protectionName)
}
}
return nil
}

func (c *defaultShieldController) getCurrentShieldAvailability(ctx context.Context) (bool, error) {
cachedShieldAvailable, exists := c.shieldAvailableCache.Get(shieldAvailableCacheKey)
if exists {
available, err := strconv.ParseBool(cachedShieldAvailable.(string))
if err == nil {
return available, nil
}
}

available, err := c.cloud.ShieldAvailable(ctx)
if err != nil {
return false, fmt.Errorf("failed to get shield advanced subscription state %v", err)
}
c.shieldAvailableCache.Add(shieldAvailableCacheKey, "true", shieldAvailableCacheTTL)
return available, nil
}

func (c *defaultShieldController) getCurrentProtectionStatus(ctx context.Context, lbArn string) (*shield.Protection, error) {
cachedProtection, exists := c.protectionEnabledForLBCache.Get(lbArn)
if exists {
return cachedProtection.(*shield.Protection), nil
}

protection, err := c.cloud.GetProtection(ctx, aws.String(lbArn))
if err != nil {
return nil, errors.Wrapf(err, "failed to get protection status for load balancer %v", lbArn)
}

c.protectionEnabledForLBCache.Add(lbArn, protection, protectionEnabledForLBCacheTTL)
return protection, nil
}
Loading