Skip to content

Commit

Permalink
Merge pull request #1211 from Vlaaaaaaad/waf-v2-clean
Browse files Browse the repository at this point in the history
WAFv2 support
  • Loading branch information
k8s-ci-robot authored Apr 18, 2020
2 parents 25d2b0e + 7a873c4 commit b2a4dbf
Show file tree
Hide file tree
Showing 14 changed files with 3,451 additions and 40 deletions.
10 changes: 10 additions & 0 deletions docs/examples/iam-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"wafv2:GetWebACL",
"wafv2:GetWebACLForResource",
"wafv2:AssociateWebACL",
"wafv2:DisassociateWebACL"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/ingress/annotation.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ You can add kubernetes annotations to ingress and service objects to customize t
|[alb.ingress.kubernetes.io/target-type](#target-type)|instance \| ip|instance|ingress,service|
|[alb.ingress.kubernetes.io/unhealthy-threshold-count](#unhealthy-threshold-count)|integer|'2'|ingress,service|
|[alb.ingress.kubernetes.io/waf-acl-id](#waf-acl-id)|string|N/A|ingress|
|[alb.ingress.kubernetes.io/wafv2-acl-arn](#wafv2-acl-arn)|string|N/A|ingress|

## Traffic Listening
Traffic Listening can be controlled with following annotations:
Expand Down Expand Up @@ -497,6 +498,19 @@ Health check on target groups can be controlled with following annotations:
```alb.ingress.kubernetes.io/waf-acl-id: 499e8b99-6671-4614-a86d-adb1810b7fbe
```

## WAFv2
- <a name="wafv2-acl-arn">`alb.ingress.kubernetes.io/wafv2-acl-arn`</a> specifies ARN for the Amazon WAFv2 web ACL.

!!!warning ""
Only Regional WAFv2 is supported.

!!!example
```alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:us-west-2:xxxxx:regional/webacl/xxxxxxx/3ab78708-85b0-49d3-b4e1-7a9615a6613b
```

!!!tip ""
To get the WAFv2 Web ACL ARN from the Console, click the gear icon in the upper right and enable the ARN column.

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

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)
wafV2Controller := NewWAFV2Controller(cloud)
shieldController := NewShieldController(cloud)

return &defaultController{
Expand All @@ -58,6 +59,7 @@ func NewController(
tagsController: tagsController,
attrsController: attrsController,
wafController: wafController,
wafV2Controller: wafV2Controller,
shieldController: shieldController,
}
}
Expand All @@ -83,6 +85,7 @@ type defaultController struct {
tagsController tags.Controller
attrsController AttributesController
wafController WAFController
wafV2Controller WAFV2Controller
shieldController ShieldController
}

Expand Down Expand Up @@ -122,6 +125,12 @@ func (controller *defaultController) Reconcile(ctx context.Context, ingress *ext
}
}

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

if controller.store.GetConfig().FeatureGate.Enabled(config.ShieldAdvanced) {
if err := controller.shieldController.Reconcile(ctx, lbArn, ingress); err != nil {
return nil, err
Expand Down
92 changes: 92 additions & 0 deletions internal/alb/lb/wafv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package lb

import (
"context"
"time"

"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/albctx"
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws"
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/ingress/annotations"
"github.com/pkg/errors"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/util/cache"
)

const (
webACLARNForLBCacheMaxSize = 1024
webACLARNForLBCacheTTL = 10 * time.Minute
)

// WAFCV2ontroller provides functionality to manage ALB's WAF V2 associations.
type WAFV2Controller interface {
Reconcile(ctx context.Context, lbArn string, ingress *extensions.Ingress) error
}

func NewWAFV2Controller(cloud aws.CloudAPI) WAFV2Controller {
return &defaultWAFV2Controller{
cloud: cloud,
webACLARNForLBCache: cache.NewLRUExpireCache(webACLARNForLBCacheMaxSize),
}
}

type defaultWAFV2Controller struct {
cloud aws.CloudAPI

// cache that stores webACLARNForLBCache for LoadBalancerARN.
// The cache value is string, while "" represents no webACL.
webACLARNForLBCache *cache.LRUExpireCache
}

func (c *defaultWAFV2Controller) Reconcile(ctx context.Context, lbArn string, ing *extensions.Ingress) error {
var desiredWebACLARN string

_ = annotations.LoadStringAnnotation("wafv2-acl-arn", &desiredWebACLARN, ing.Annotations)

currentWebACLId, err := c.getCurrentWebACLARN(ctx, lbArn)
if err != nil {
return err
}

switch {
case desiredWebACLARN == "" && currentWebACLId != "":
albctx.GetLogger(ctx).Infof("disassociate WAFv2 webACL on %v", lbArn)
if _, err := c.cloud.DisassociateWAFV2(ctx, aws.String(lbArn)); err != nil {
return errors.Wrapf(err, "failed to disassociate WAFv2 webACL on LoadBalancer %v", lbArn)
}
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL)
case desiredWebACLARN != "" && currentWebACLId != "" && desiredWebACLARN != currentWebACLId:
albctx.GetLogger(ctx).Infof("change WAFv2 webACL on %v from %v to %v", lbArn, currentWebACLId, desiredWebACLARN)
if _, err := c.cloud.AssociateWAFV2(ctx, aws.String(lbArn), aws.String(desiredWebACLARN)); err != nil {
return errors.Wrapf(err, "failed to associate WAFv2 webACL on LoadBalancer %v", lbArn)
}
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL)
case desiredWebACLARN != "" && currentWebACLId == "":
albctx.GetLogger(ctx).Infof("associate WAFv2 webACL %v on %v", desiredWebACLARN, lbArn)
if _, err := c.cloud.AssociateWAFV2(ctx, aws.String(lbArn), aws.String(desiredWebACLARN)); err != nil {
return errors.Wrapf(err, "failed to associate WAFv2 webACL on LoadBalancer %v", lbArn)
}
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL)
}

return nil
}

func (c *defaultWAFV2Controller) getCurrentWebACLARN(ctx context.Context, lbArn string) (string, error) {
cachedWebACLARN, exists := c.webACLARNForLBCache.Get(lbArn)
if exists {
return cachedWebACLARN.(string), nil
}

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

var webACLARN string
if webACL != nil {
webACLARN = aws.StringValue(webACL.ARN)
}

c.webACLARNForLBCache.Add(lbArn, webACLARN, webACLARNForLBCacheTTL)
return webACLARN, nil
}
205 changes: 205 additions & 0 deletions internal/alb/lb/wafv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package lb

import (
"context"
"testing"

"k8s.io/apimachinery/pkg/util/intstr"

"github.com/aws/aws-sdk-go/service/wafv2"
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws"
"github.com/kubernetes-sigs/aws-alb-ingress-controller/mocks"
"github.com/stretchr/testify/assert"
apiv1 "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func buildWAFV2TestIngress(wafIngressAnnotations map[string]string) *extensions.Ingress {
defaultBackend := extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}

return &extensions.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: apiv1.NamespaceDefault,
Annotations: wafIngressAnnotations,
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
Rules: []extensions.IngressRule{
{
Host: "foo.bar.com",
IngressRuleValue: extensions.IngressRuleValue{
HTTP: &extensions.HTTPIngressRuleValue{
Paths: []extensions.HTTPIngressPath{
{
Path: "/foo",
Backend: defaultBackend,
},
},
},
},
},
},
},
}
}

func Test_defaultWAFV2Controller_Reconcile(t *testing.T) {
for _, tc := range []struct {
Name string
GetWAFV2WebACLSummaryResponse *wafv2.WebACL
GetWAFV2WebACLSummaryError error
AssociateWAFV2Response *wafv2.AssociateWebACLOutput
AssociateWAFV2Error error
DisassociateWAFV2Response *wafv2.DisassociateWebACLOutput
DisassociateWAFV2Error error
Expected error
ExpectedError error
LoadBalancerARN string
IngressAnnotations *extensions.Ingress
DesiredWebACLARN string
GetWAFV2WebACLSummaryTimesCalled int
AssociateWAFV2TimesCalled int
DisassociateWAFV2TimesCalled int
}{
{
Name: "No annotation, confirm nothing is attached",
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
ARN: nil,
},
GetWAFV2WebACLSummaryError: nil,
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
AssociateWAFV2Error: nil,
Expected: nil,
LoadBalancerARN: "arn:lb",
IngressAnnotations: buildWAFV2TestIngress(
map[string]string{},
),
DesiredWebACLARN: "",
GetWAFV2WebACLSummaryTimesCalled: 1,
AssociateWAFV2TimesCalled: 0,
DisassociateWAFV2TimesCalled: 0,
},
{
Name: "Empty WAFv2 annotation",
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
ARN: nil,
},
GetWAFV2WebACLSummaryError: nil,
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
AssociateWAFV2Error: nil,
Expected: nil,
LoadBalancerARN: "arn:lb",
IngressAnnotations: buildWAFV2TestIngress(
map[string]string{
"alb.ingress.kubernetes.io/wafv2-acl-arn": "",
},
),
DesiredWebACLARN: "",
GetWAFV2WebACLSummaryTimesCalled: 1,
AssociateWAFV2TimesCalled: 0,
DisassociateWAFV2TimesCalled: 0,
},
{
Name: "No annotation, detach WAFv2",
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a"),
},
GetWAFV2WebACLSummaryError: nil,
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
AssociateWAFV2Error: nil,
Expected: nil,
LoadBalancerARN: "arn:lb",
IngressAnnotations: buildWAFV2TestIngress(
map[string]string{},
),
DesiredWebACLARN: "",
GetWAFV2WebACLSummaryTimesCalled: 1,
AssociateWAFV2TimesCalled: 0,
DisassociateWAFV2TimesCalled: 1,
},
{
Name: "Empty annotation, dissassociate WAFv2",
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a"),
},
GetWAFV2WebACLSummaryError: nil,
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
AssociateWAFV2Error: nil,
Expected: nil,
LoadBalancerARN: "arn:lb",
IngressAnnotations: buildWAFV2TestIngress(
map[string]string{
"alb.ingress.kubernetes.io/wafv2-acl-arn": "",
},
),
DesiredWebACLARN: "",
GetWAFV2WebACLSummaryTimesCalled: 1,
AssociateWAFV2TimesCalled: 0,
DisassociateWAFV2TimesCalled: 1,
},
{
Name: "Annotation, associate WAFv2",
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
ARN: nil,
},
GetWAFV2WebACLSummaryError: nil,
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
AssociateWAFV2Error: nil,
Expected: nil,
LoadBalancerARN: "arn:lb",
IngressAnnotations: buildWAFV2TestIngress(
map[string]string{
"alb.ingress.kubernetes.io/wafv2-acl-arn": "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
},
),
DesiredWebACLARN: "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
GetWAFV2WebACLSummaryTimesCalled: 1,
AssociateWAFV2TimesCalled: 1,
DisassociateWAFV2TimesCalled: 0,
},
{
Name: "Annotation, change WAFv2",
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0bb00000-00b0-00b0-b0b0-0b0000a0000b"),
},
GetWAFV2WebACLSummaryError: nil,
Expected: nil,
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
AssociateWAFV2Error: nil,
LoadBalancerARN: "arn:lb",
IngressAnnotations: buildWAFV2TestIngress(
map[string]string{
"alb.ingress.kubernetes.io/wafv2-acl-arn": "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
},
),
DesiredWebACLARN: "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
GetWAFV2WebACLSummaryTimesCalled: 1,
AssociateWAFV2TimesCalled: 1,
DisassociateWAFV2TimesCalled: 0,
},
} {
t.Run(tc.Name, func(t *testing.T) {
ctx := context.Background()
cloud := &mocks.CloudAPI{}

cloud.On("GetWAFV2WebACLSummary", ctx, aws.String(tc.LoadBalancerARN)).Return(tc.GetWAFV2WebACLSummaryResponse, tc.GetWAFV2WebACLSummaryError)
cloud.On("AssociateWAFV2", ctx, aws.String(tc.LoadBalancerARN), aws.String(tc.DesiredWebACLARN)).Return(tc.AssociateWAFV2Response, tc.AssociateWAFV2Error)
cloud.On("DisassociateWAFV2", ctx, aws.String(tc.LoadBalancerARN)).Return(tc.DisassociateWAFV2Response, tc.DisassociateWAFV2Error)

controller := NewWAFV2Controller(cloud)
err := controller.Reconcile(ctx, tc.LoadBalancerARN, tc.IngressAnnotations)
assert.Equal(t, tc.Expected, err)
cloud.AssertNumberOfCalls(t, "GetWAFV2WebACLSummary", tc.GetWAFV2WebACLSummaryTimesCalled)
cloud.AssertNumberOfCalls(t, "AssociateWAFV2", tc.AssociateWAFV2TimesCalled)
cloud.AssertNumberOfCalls(t, "DisassociateWAFV2", tc.DisassociateWAFV2TimesCalled)
})
}
}
Loading

0 comments on commit b2a4dbf

Please sign in to comment.