Skip to content

Commit d544b7e

Browse files
authored
Merge pull request kubernetes-sigs#1211 from Vlaaaaaaad/waf-v2-clean
WAFv2 support
2 parents e69b456 + 5af0a97 commit d544b7e

File tree

14 files changed

+3451
-40
lines changed

14 files changed

+3451
-40
lines changed

docs/examples/iam-policy.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@
114114
],
115115
"Resource": "*"
116116
},
117+
{
118+
"Effect": "Allow",
119+
"Action": [
120+
"wafv2:GetWebACL",
121+
"wafv2:GetWebACLForResource",
122+
"wafv2:AssociateWebACL",
123+
"wafv2:DisassociateWebACL"
124+
],
125+
"Resource": "*"
126+
},
117127
{
118128
"Effect": "Allow",
119129
"Action": [

docs/guide/ingress/annotation.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ You can add kubernetes annotations to ingress and service objects to customize t
4747
|[alb.ingress.kubernetes.io/target-type](#target-type)|instance \| ip|instance|ingress,service|
4848
|[alb.ingress.kubernetes.io/unhealthy-threshold-count](#unhealthy-threshold-count)|integer|'2'|ingress,service|
4949
|[alb.ingress.kubernetes.io/waf-acl-id](#waf-acl-id)|string|N/A|ingress|
50+
|[alb.ingress.kubernetes.io/wafv2-acl-arn](#wafv2-acl-arn)|string|N/A|ingress|
5051

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

501+
## WAFv2
502+
- <a name="wafv2-acl-arn">`alb.ingress.kubernetes.io/wafv2-acl-arn`</a> specifies ARN for the Amazon WAFv2 web ACL.
503+
504+
!!!warning ""
505+
Only Regional WAFv2 is supported.
506+
507+
!!!example
508+
```alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:us-west-2:xxxxx:regional/webacl/xxxxxxx/3ab78708-85b0-49d3-b4e1-7a9615a6613b
509+
```
510+
511+
!!!tip ""
512+
To get the WAFv2 Web ACL ARN from the Console, click the gear icon in the upper right and enable the ARN column.
513+
500514
## Shield Advanced
501515
- <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.
502516

internal/alb/lb/loadbalancer.go

Lines changed: 9 additions & 0 deletions
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+
wafV2Controller := NewWAFV2Controller(cloud)
4950
shieldController := NewShieldController(cloud)
5051

5152
return &defaultController{
@@ -58,6 +59,7 @@ func NewController(
5859
tagsController: tagsController,
5960
attrsController: attrsController,
6061
wafController: wafController,
62+
wafV2Controller: wafV2Controller,
6163
shieldController: shieldController,
6264
}
6365
}
@@ -83,6 +85,7 @@ type defaultController struct {
8385
tagsController tags.Controller
8486
attrsController AttributesController
8587
wafController WAFController
88+
wafV2Controller WAFV2Controller
8689
shieldController ShieldController
8790
}
8891

@@ -122,6 +125,12 @@ func (controller *defaultController) Reconcile(ctx context.Context, ingress *ext
122125
}
123126
}
124127

128+
if controller.store.GetConfig().FeatureGate.Enabled(config.WAFV2) {
129+
if err := controller.wafV2Controller.Reconcile(ctx, lbArn, ingress); err != nil {
130+
return nil, err
131+
}
132+
}
133+
125134
if controller.store.GetConfig().FeatureGate.Enabled(config.ShieldAdvanced) {
126135
if err := controller.shieldController.Reconcile(ctx, lbArn, ingress); err != nil {
127136
return nil, err

internal/alb/lb/wafv2.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package lb
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/albctx"
8+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws"
9+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/ingress/annotations"
10+
"github.com/pkg/errors"
11+
extensions "k8s.io/api/extensions/v1beta1"
12+
"k8s.io/apimachinery/pkg/util/cache"
13+
)
14+
15+
const (
16+
webACLARNForLBCacheMaxSize = 1024
17+
webACLARNForLBCacheTTL = 10 * time.Minute
18+
)
19+
20+
// WAFCV2ontroller provides functionality to manage ALB's WAF V2 associations.
21+
type WAFV2Controller interface {
22+
Reconcile(ctx context.Context, lbArn string, ingress *extensions.Ingress) error
23+
}
24+
25+
func NewWAFV2Controller(cloud aws.CloudAPI) WAFV2Controller {
26+
return &defaultWAFV2Controller{
27+
cloud: cloud,
28+
webACLARNForLBCache: cache.NewLRUExpireCache(webACLARNForLBCacheMaxSize),
29+
}
30+
}
31+
32+
type defaultWAFV2Controller struct {
33+
cloud aws.CloudAPI
34+
35+
// cache that stores webACLARNForLBCache for LoadBalancerARN.
36+
// The cache value is string, while "" represents no webACL.
37+
webACLARNForLBCache *cache.LRUExpireCache
38+
}
39+
40+
func (c *defaultWAFV2Controller) Reconcile(ctx context.Context, lbArn string, ing *extensions.Ingress) error {
41+
var desiredWebACLARN string
42+
43+
_ = annotations.LoadStringAnnotation("wafv2-acl-arn", &desiredWebACLARN, ing.Annotations)
44+
45+
currentWebACLId, err := c.getCurrentWebACLARN(ctx, lbArn)
46+
if err != nil {
47+
return err
48+
}
49+
50+
switch {
51+
case desiredWebACLARN == "" && currentWebACLId != "":
52+
albctx.GetLogger(ctx).Infof("disassociate WAFv2 webACL on %v", lbArn)
53+
if _, err := c.cloud.DisassociateWAFV2(ctx, aws.String(lbArn)); err != nil {
54+
return errors.Wrapf(err, "failed to disassociate WAFv2 webACL on LoadBalancer %v", lbArn)
55+
}
56+
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL)
57+
case desiredWebACLARN != "" && currentWebACLId != "" && desiredWebACLARN != currentWebACLId:
58+
albctx.GetLogger(ctx).Infof("change WAFv2 webACL on %v from %v to %v", lbArn, currentWebACLId, desiredWebACLARN)
59+
if _, err := c.cloud.AssociateWAFV2(ctx, aws.String(lbArn), aws.String(desiredWebACLARN)); err != nil {
60+
return errors.Wrapf(err, "failed to associate WAFv2 webACL on LoadBalancer %v", lbArn)
61+
}
62+
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL)
63+
case desiredWebACLARN != "" && currentWebACLId == "":
64+
albctx.GetLogger(ctx).Infof("associate WAFv2 webACL %v on %v", desiredWebACLARN, lbArn)
65+
if _, err := c.cloud.AssociateWAFV2(ctx, aws.String(lbArn), aws.String(desiredWebACLARN)); err != nil {
66+
return errors.Wrapf(err, "failed to associate WAFv2 webACL on LoadBalancer %v", lbArn)
67+
}
68+
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL)
69+
}
70+
71+
return nil
72+
}
73+
74+
func (c *defaultWAFV2Controller) getCurrentWebACLARN(ctx context.Context, lbArn string) (string, error) {
75+
cachedWebACLARN, exists := c.webACLARNForLBCache.Get(lbArn)
76+
if exists {
77+
return cachedWebACLARN.(string), nil
78+
}
79+
80+
webACL, err := c.cloud.GetWAFV2WebACLSummary(ctx, aws.String(lbArn))
81+
if err != nil {
82+
return "", errors.Wrapf(err, "failed get WAFv2 webACL for load balancer %v", lbArn)
83+
}
84+
85+
var webACLARN string
86+
if webACL != nil {
87+
webACLARN = aws.StringValue(webACL.ARN)
88+
}
89+
90+
c.webACLARNForLBCache.Add(lbArn, webACLARN, webACLARNForLBCacheTTL)
91+
return webACLARN, nil
92+
}

internal/alb/lb/wafv2_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package lb
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"k8s.io/apimachinery/pkg/util/intstr"
8+
9+
"github.com/aws/aws-sdk-go/service/wafv2"
10+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws"
11+
"github.com/kubernetes-sigs/aws-alb-ingress-controller/mocks"
12+
"github.com/stretchr/testify/assert"
13+
apiv1 "k8s.io/api/core/v1"
14+
extensions "k8s.io/api/extensions/v1beta1"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
)
17+
18+
func buildWAFV2TestIngress(wafIngressAnnotations map[string]string) *extensions.Ingress {
19+
defaultBackend := extensions.IngressBackend{
20+
ServiceName: "default-backend",
21+
ServicePort: intstr.FromInt(80),
22+
}
23+
24+
return &extensions.Ingress{
25+
ObjectMeta: metav1.ObjectMeta{
26+
Name: "foo",
27+
Namespace: apiv1.NamespaceDefault,
28+
Annotations: wafIngressAnnotations,
29+
},
30+
Spec: extensions.IngressSpec{
31+
Backend: &extensions.IngressBackend{
32+
ServiceName: "default-backend",
33+
ServicePort: intstr.FromInt(80),
34+
},
35+
Rules: []extensions.IngressRule{
36+
{
37+
Host: "foo.bar.com",
38+
IngressRuleValue: extensions.IngressRuleValue{
39+
HTTP: &extensions.HTTPIngressRuleValue{
40+
Paths: []extensions.HTTPIngressPath{
41+
{
42+
Path: "/foo",
43+
Backend: defaultBackend,
44+
},
45+
},
46+
},
47+
},
48+
},
49+
},
50+
},
51+
}
52+
}
53+
54+
func Test_defaultWAFV2Controller_Reconcile(t *testing.T) {
55+
for _, tc := range []struct {
56+
Name string
57+
GetWAFV2WebACLSummaryResponse *wafv2.WebACL
58+
GetWAFV2WebACLSummaryError error
59+
AssociateWAFV2Response *wafv2.AssociateWebACLOutput
60+
AssociateWAFV2Error error
61+
DisassociateWAFV2Response *wafv2.DisassociateWebACLOutput
62+
DisassociateWAFV2Error error
63+
Expected error
64+
ExpectedError error
65+
LoadBalancerARN string
66+
IngressAnnotations *extensions.Ingress
67+
DesiredWebACLARN string
68+
GetWAFV2WebACLSummaryTimesCalled int
69+
AssociateWAFV2TimesCalled int
70+
DisassociateWAFV2TimesCalled int
71+
}{
72+
{
73+
Name: "No annotation, confirm nothing is attached",
74+
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
75+
ARN: nil,
76+
},
77+
GetWAFV2WebACLSummaryError: nil,
78+
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
79+
AssociateWAFV2Error: nil,
80+
Expected: nil,
81+
LoadBalancerARN: "arn:lb",
82+
IngressAnnotations: buildWAFV2TestIngress(
83+
map[string]string{},
84+
),
85+
DesiredWebACLARN: "",
86+
GetWAFV2WebACLSummaryTimesCalled: 1,
87+
AssociateWAFV2TimesCalled: 0,
88+
DisassociateWAFV2TimesCalled: 0,
89+
},
90+
{
91+
Name: "Empty WAFv2 annotation",
92+
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
93+
ARN: nil,
94+
},
95+
GetWAFV2WebACLSummaryError: nil,
96+
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
97+
AssociateWAFV2Error: nil,
98+
Expected: nil,
99+
LoadBalancerARN: "arn:lb",
100+
IngressAnnotations: buildWAFV2TestIngress(
101+
map[string]string{
102+
"alb.ingress.kubernetes.io/wafv2-acl-arn": "",
103+
},
104+
),
105+
DesiredWebACLARN: "",
106+
GetWAFV2WebACLSummaryTimesCalled: 1,
107+
AssociateWAFV2TimesCalled: 0,
108+
DisassociateWAFV2TimesCalled: 0,
109+
},
110+
{
111+
Name: "No annotation, detach WAFv2",
112+
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
113+
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a"),
114+
},
115+
GetWAFV2WebACLSummaryError: nil,
116+
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
117+
AssociateWAFV2Error: nil,
118+
Expected: nil,
119+
LoadBalancerARN: "arn:lb",
120+
IngressAnnotations: buildWAFV2TestIngress(
121+
map[string]string{},
122+
),
123+
DesiredWebACLARN: "",
124+
GetWAFV2WebACLSummaryTimesCalled: 1,
125+
AssociateWAFV2TimesCalled: 0,
126+
DisassociateWAFV2TimesCalled: 1,
127+
},
128+
{
129+
Name: "Empty annotation, dissassociate WAFv2",
130+
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
131+
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a"),
132+
},
133+
GetWAFV2WebACLSummaryError: nil,
134+
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
135+
AssociateWAFV2Error: nil,
136+
Expected: nil,
137+
LoadBalancerARN: "arn:lb",
138+
IngressAnnotations: buildWAFV2TestIngress(
139+
map[string]string{
140+
"alb.ingress.kubernetes.io/wafv2-acl-arn": "",
141+
},
142+
),
143+
DesiredWebACLARN: "",
144+
GetWAFV2WebACLSummaryTimesCalled: 1,
145+
AssociateWAFV2TimesCalled: 0,
146+
DisassociateWAFV2TimesCalled: 1,
147+
},
148+
{
149+
Name: "Annotation, associate WAFv2",
150+
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
151+
ARN: nil,
152+
},
153+
GetWAFV2WebACLSummaryError: nil,
154+
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
155+
AssociateWAFV2Error: nil,
156+
Expected: nil,
157+
LoadBalancerARN: "arn:lb",
158+
IngressAnnotations: buildWAFV2TestIngress(
159+
map[string]string{
160+
"alb.ingress.kubernetes.io/wafv2-acl-arn": "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
161+
},
162+
),
163+
DesiredWebACLARN: "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
164+
GetWAFV2WebACLSummaryTimesCalled: 1,
165+
AssociateWAFV2TimesCalled: 1,
166+
DisassociateWAFV2TimesCalled: 0,
167+
},
168+
{
169+
Name: "Annotation, change WAFv2",
170+
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{
171+
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0bb00000-00b0-00b0-b0b0-0b0000a0000b"),
172+
},
173+
GetWAFV2WebACLSummaryError: nil,
174+
Expected: nil,
175+
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{},
176+
AssociateWAFV2Error: nil,
177+
LoadBalancerARN: "arn:lb",
178+
IngressAnnotations: buildWAFV2TestIngress(
179+
map[string]string{
180+
"alb.ingress.kubernetes.io/wafv2-acl-arn": "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
181+
},
182+
),
183+
DesiredWebACLARN: "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a",
184+
GetWAFV2WebACLSummaryTimesCalled: 1,
185+
AssociateWAFV2TimesCalled: 1,
186+
DisassociateWAFV2TimesCalled: 0,
187+
},
188+
} {
189+
t.Run(tc.Name, func(t *testing.T) {
190+
ctx := context.Background()
191+
cloud := &mocks.CloudAPI{}
192+
193+
cloud.On("GetWAFV2WebACLSummary", ctx, aws.String(tc.LoadBalancerARN)).Return(tc.GetWAFV2WebACLSummaryResponse, tc.GetWAFV2WebACLSummaryError)
194+
cloud.On("AssociateWAFV2", ctx, aws.String(tc.LoadBalancerARN), aws.String(tc.DesiredWebACLARN)).Return(tc.AssociateWAFV2Response, tc.AssociateWAFV2Error)
195+
cloud.On("DisassociateWAFV2", ctx, aws.String(tc.LoadBalancerARN)).Return(tc.DisassociateWAFV2Response, tc.DisassociateWAFV2Error)
196+
197+
controller := NewWAFV2Controller(cloud)
198+
err := controller.Reconcile(ctx, tc.LoadBalancerARN, tc.IngressAnnotations)
199+
assert.Equal(t, tc.Expected, err)
200+
cloud.AssertNumberOfCalls(t, "GetWAFV2WebACLSummary", tc.GetWAFV2WebACLSummaryTimesCalled)
201+
cloud.AssertNumberOfCalls(t, "AssociateWAFV2", tc.AssociateWAFV2TimesCalled)
202+
cloud.AssertNumberOfCalls(t, "DisassociateWAFV2", tc.DisassociateWAFV2TimesCalled)
203+
})
204+
}
205+
}

0 commit comments

Comments
 (0)