Skip to content

Commit 068ace0

Browse files
authored
Merge pull request #39 from smalruby/feature/waf-origin-restriction
feat: add WAF to restrict AppSync origins in production
2 parents 9818fca + e8cdbb0 commit 068ace0

3 files changed

Lines changed: 156 additions & 16 deletions

File tree

lib/mesh-v2-stack.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as cdk from 'aws-cdk-lib/core';
22
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
33
import * as appsync from 'aws-cdk-lib/aws-appsync';
44
import * as lambda from 'aws-cdk-lib/aws-lambda';
5+
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
56
import * as route53 from 'aws-cdk-lib/aws-route53';
67
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
78
import * as targets from 'aws-cdk-lib/aws-route53-targets';
@@ -131,6 +132,62 @@ export class MeshV2Stack extends cdk.Stack {
131132
},
132133
});
133134

135+
// WAF configuration (Only for production)
136+
if (stage === 'prod') {
137+
const allowedOrigins = [
138+
'https://smalruby.app',
139+
'https://smalruby.jp'
140+
];
141+
142+
const webAcl = new wafv2.CfnWebACL(this, 'MeshV2ApiWebAcl', {
143+
defaultAction: { block: {} },
144+
scope: 'REGIONAL',
145+
visibilityConfig: {
146+
cloudWatchMetricsEnabled: true,
147+
metricName: 'MeshV2ApiWebAcl',
148+
sampledRequestsEnabled: true,
149+
},
150+
rules: [
151+
{
152+
name: 'AllowSpecificOrigins',
153+
priority: 1,
154+
action: { allow: {} },
155+
statement: {
156+
orStatement: {
157+
statements: allowedOrigins.map(origin => ({
158+
byteMatchStatement: {
159+
fieldToMatch: {
160+
singleHeader: {
161+
name: 'origin',
162+
},
163+
},
164+
positionalConstraint: 'EXACTLY',
165+
searchString: origin,
166+
textTransformations: [
167+
{
168+
priority: 0,
169+
type: 'LOWERCASE',
170+
},
171+
],
172+
},
173+
})),
174+
},
175+
},
176+
visibilityConfig: {
177+
cloudWatchMetricsEnabled: true,
178+
metricName: 'AllowSpecificOrigins',
179+
sampledRequestsEnabled: true,
180+
},
181+
},
182+
],
183+
});
184+
185+
new wafv2.CfnWebACLAssociation(this, 'MeshV2ApiWebAclAssociation', {
186+
resourceArn: this.api.arn,
187+
webAclArn: webAcl.attrArn,
188+
});
189+
}
190+
134191
// Route53 Alias record for Custom Domain
135192
if (customDomain && zone) {
136193
// Extract subdomain from customDomain (e.g., "graphql.api.smalruby.app" -> "graphql")

test/mesh-v2.test.ts

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,99 @@
1-
// import * as cdk from 'aws-cdk-lib/core';
2-
// import { Template } from 'aws-cdk-lib/assertions';
3-
// import * as MeshV2 from '../lib/mesh-v2-stack';
4-
5-
// example test. To run these tests, uncomment this file along with the
6-
// example resource in lib/mesh-v2-stack.ts
7-
test('SQS Queue Created', () => {
8-
// const app = new cdk.App();
9-
// // WHEN
10-
// const stack = new MeshV2.MeshV2Stack(app, 'MyTestStack');
11-
// // THEN
12-
// const template = Template.fromStack(stack);
13-
14-
// template.hasResourceProperties('AWS::SQS::Queue', {
15-
// VisibilityTimeout: 300
16-
// });
1+
import * as cdk from 'aws-cdk-lib/core';
2+
import { Template } from 'aws-cdk-lib/assertions';
3+
import * as MeshV2 from '../lib/mesh-v2-stack';
4+
5+
describe('MeshV2Stack', () => {
6+
test('AppSync API and DynamoDB Table Created', () => {
7+
const app = new cdk.App();
8+
const stack = new MeshV2.MeshV2Stack(app, 'MyTestStack', {
9+
env: { account: '123456789012', region: 'us-east-1' }
10+
});
11+
const template = Template.fromStack(stack);
12+
13+
template.hasResourceProperties('AWS::DynamoDB::Table', {
14+
TableName: 'MeshV2Table-stg'
15+
});
16+
17+
template.hasResourceProperties('AWS::AppSync::GraphQLApi', {
18+
Name: 'MeshV2Api-stg'
19+
});
20+
});
21+
22+
test('WAF is created when stage is prod', () => {
23+
const app = new cdk.App({
24+
context: {
25+
stage: 'prod'
26+
}
27+
});
28+
const stack = new MeshV2.MeshV2Stack(app, 'MyProdTestStack', {
29+
env: { account: '123456789012', region: 'us-east-1' }
30+
});
31+
const template = Template.fromStack(stack);
32+
33+
template.resourceCountIs('AWS::WAFv2::WebACL', 1);
34+
template.hasResourceProperties('AWS::WAFv2::WebACL', {
35+
DefaultAction: { Block: {} },
36+
Scope: 'REGIONAL',
37+
Rules: [
38+
{
39+
Name: 'AllowSpecificOrigins',
40+
Priority: 1,
41+
Action: { Allow: {} },
42+
Statement: {
43+
OrStatement: {
44+
Statements: [
45+
{
46+
ByteMatchStatement: {
47+
FieldToMatch: {
48+
SingleHeader: { name: 'origin' }
49+
},
50+
PositionalConstraint: 'EXACTLY',
51+
SearchString: 'https://smalruby.app',
52+
TextTransformations: [
53+
{
54+
Priority: 0,
55+
Type: 'LOWERCASE'
56+
}
57+
]
58+
}
59+
},
60+
{
61+
ByteMatchStatement: {
62+
FieldToMatch: {
63+
SingleHeader: { name: 'origin' }
64+
},
65+
PositionalConstraint: 'EXACTLY',
66+
SearchString: 'https://smalruby.jp',
67+
TextTransformations: [
68+
{
69+
Priority: 0,
70+
Type: 'LOWERCASE'
71+
}
72+
]
73+
}
74+
}
75+
]
76+
}
77+
}
78+
}
79+
]
80+
});
81+
82+
template.resourceCountIs('AWS::WAFv2::WebACLAssociation', 1);
83+
});
84+
85+
test('WAF is not created when stage is stg', () => {
86+
const app = new cdk.App({
87+
context: {
88+
stage: 'stg'
89+
}
90+
});
91+
const stack = new MeshV2.MeshV2Stack(app, 'MyStgTestStack', {
92+
env: { account: '123456789012', region: 'us-east-1' }
93+
});
94+
const template = Template.fromStack(stack);
95+
96+
template.resourceCountIs('AWS::WAFv2::WebACL', 0);
97+
template.resourceCountIs('AWS::WAFv2::WebACLAssociation', 0);
98+
});
1799
});

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"experimentalDecorators": true,
2222
"strictPropertyInitialization": false,
2323
"skipLibCheck": true,
24+
"isolatedModules": true,
2425
"typeRoots": [
2526
"./node_modules/@types"
2627
]

0 commit comments

Comments
 (0)