raindancers-cloudfront 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bicep/azure-functions/custom-claims-provider/function_app.py +164 -0
- package/lib/bicep/azure-functions/custom-claims-provider/host.json +10 -0
- package/lib/bicep/azure-functions/custom-claims-provider/requirements.txt +8 -0
- package/lib/bicep/deploy/lambda/Dockerfile +10 -0
- package/lib/bicep/deploy/lambda/index.py +341 -0
- package/lib/bicep/patterns/scripts/LOCALDEBUGREADME.md +86 -0
- package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh +122 -0
- package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh.bak +118 -0
- package/lib/cloudfront/cloudfront-functions/auth-check.js +220 -0
- package/lib/cloudfront/cloudfront-functions/modules/auth-check.js +263 -0
- package/lib/cloudfront/cloudfront-functions/modules/cognito-auth-check.js +223 -0
- package/lib/cloudfront/cloudfront-functions/modules/shared-utils.js +47 -0
- package/lib/cloudfront/cloudfront-functions/modules/tls-check.js +39 -0
- package/lib/cloudfront/cloudfront-functions/modules/url-rewrite.js +6 -0
- package/lib/cloudfront/cloudfront-functions/role-enforcer.js +93 -0
- package/lib/cloudfront/cloudfront-functions/tls-enforcer.js +55 -0
- package/lib/cloudfront/cloudfront-functions/userinfo-endpoint.js +208 -0
- package/lib/cloudfront/lambda/certificate/index.py +219 -0
- package/lib/cloudfront/lambda/cognito-auth/oauth-callback.py +215 -0
- package/lib/cloudfront/lambda/cognito-auth/requirements.txt +3 -0
- package/lib/cloudfront/lambda/edge-auth/config.py +6 -0
- package/lib/cloudfront/lambda/edge-auth/config_generated.py +12 -0
- package/lib/cloudfront/lambda/edge-auth/oauth-callback.py +538 -0
- package/lib/cloudfront/lambda/edge-auth/requirements.txt +3 -0
- package/lib/cloudfront/lambda/hmacSecret/index.py +129 -0
- package/lib/cloudfront/lambda/hmacSecret/requirements.txt +1 -0
- package/lib/cloudfront/lambda/jwt-decoder/index.py +88 -0
- package/lib/cloudfront/lambda/pre-token/index.py +11 -0
- package/lib/cloudfront/lambda/rotateSecret/index.py +86 -0
- package/lib/cloudfront/lambda/session-revocation/index.py +80 -0
- package/lib/cloudfront/lambda/ssm-writer/index.py +44 -0
- package/lib/cloudfront/lambda/stream-processor/index.py +53 -0
- package/lib/cloudfront/lambda/webacl/index.py +356 -0
- package/lib/cloudfront/logging/README.md +144 -0
- package/lib/cloudfront/patterns/SPLIT_STACK_USAGE.md +138 -0
- package/package.json +1 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import boto3
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
wafv2 = boto3.client('wafv2', region_name='us-east-1')
|
|
6
|
+
|
|
7
|
+
def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
|
|
8
|
+
"""
|
|
9
|
+
Custom resource handler for creating WAF WebACLs in us-east-1.
|
|
10
|
+
"""
|
|
11
|
+
print(f"Event: {json.dumps(event)}")
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
request_type = event['RequestType']
|
|
15
|
+
props = event['ResourceProperties']
|
|
16
|
+
|
|
17
|
+
name = props['Name']
|
|
18
|
+
enable_managed_rules = props.get('EnableManagedRules', 'true')
|
|
19
|
+
# Convert string to boolean
|
|
20
|
+
if isinstance(enable_managed_rules, str):
|
|
21
|
+
enable_managed_rules = enable_managed_rules.lower() == 'true'
|
|
22
|
+
rate_limit = int(props.get('RateLimit', 2000))
|
|
23
|
+
path_rate_limits_str = props.get('PathRateLimits', '[]')
|
|
24
|
+
path_rate_limits = json.loads(path_rate_limits_str) if isinstance(path_rate_limits_str, str) else path_rate_limits_str
|
|
25
|
+
allowed_countries = props.get('AllowedCountries', [])
|
|
26
|
+
blocked_countries = props.get('BlockedCountries', [])
|
|
27
|
+
|
|
28
|
+
print(f"Processing {request_type} for WebACL: {name}")
|
|
29
|
+
|
|
30
|
+
if request_type == 'Create':
|
|
31
|
+
return create_webacl(name, enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
|
|
32
|
+
elif request_type == 'Update':
|
|
33
|
+
webacl_id = event['PhysicalResourceId']
|
|
34
|
+
return update_webacl(webacl_id, name, enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
|
|
35
|
+
elif request_type == 'Delete':
|
|
36
|
+
webacl_id = event['PhysicalResourceId']
|
|
37
|
+
return delete_webacl(webacl_id, name)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
'PhysicalResourceId': event.get('PhysicalResourceId', 'unknown'),
|
|
41
|
+
'Data': {}
|
|
42
|
+
}
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"ERROR: {str(e)}")
|
|
45
|
+
import traceback
|
|
46
|
+
traceback.print_exc()
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
def create_webacl(name: str, enable_managed_rules: bool, rate_limit: int, path_rate_limits: List[Dict[str, Any]], allowed_countries: List[str], blocked_countries: List[str]) -> Dict[str, Any]:
|
|
50
|
+
"""Create a WAF WebACL."""
|
|
51
|
+
|
|
52
|
+
rules = build_rules(enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
|
|
53
|
+
|
|
54
|
+
print(f"Creating WebACL with {len(rules)} rules")
|
|
55
|
+
|
|
56
|
+
response = wafv2.create_web_acl(
|
|
57
|
+
Name=name,
|
|
58
|
+
Scope='CLOUDFRONT',
|
|
59
|
+
DefaultAction={'Allow': {}},
|
|
60
|
+
Rules=rules,
|
|
61
|
+
VisibilityConfig={
|
|
62
|
+
'SampledRequestsEnabled': True,
|
|
63
|
+
'CloudWatchMetricsEnabled': True,
|
|
64
|
+
'MetricName': name
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
webacl_arn = response['Summary']['ARN']
|
|
69
|
+
webacl_id = response['Summary']['Id']
|
|
70
|
+
|
|
71
|
+
print(f"WebACL created: {webacl_arn}")
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
'PhysicalResourceId': webacl_id,
|
|
75
|
+
'Data': {
|
|
76
|
+
'WebAclArn': webacl_arn,
|
|
77
|
+
'WebAclId': webacl_id
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def update_webacl(webacl_id: str, name: str, enable_managed_rules: bool, rate_limit: int, path_rate_limits: List[Dict[str, Any]], allowed_countries: List[str], blocked_countries: List[str]) -> Dict[str, Any]:
|
|
82
|
+
"""Update a WAF WebACL."""
|
|
83
|
+
|
|
84
|
+
# Get current WebACL to get lock token
|
|
85
|
+
response = wafv2.get_web_acl(
|
|
86
|
+
Name=name,
|
|
87
|
+
Scope='CLOUDFRONT',
|
|
88
|
+
Id=webacl_id
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
lock_token = response['LockToken']
|
|
92
|
+
webacl_arn = response['WebACL']['ARN']
|
|
93
|
+
|
|
94
|
+
rules = build_rules(enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
|
|
95
|
+
|
|
96
|
+
print(f"Updating WebACL with {len(rules)} rules")
|
|
97
|
+
|
|
98
|
+
wafv2.update_web_acl(
|
|
99
|
+
Name=name,
|
|
100
|
+
Scope='CLOUDFRONT',
|
|
101
|
+
Id=webacl_id,
|
|
102
|
+
DefaultAction={'Allow': {}},
|
|
103
|
+
Rules=rules,
|
|
104
|
+
VisibilityConfig={
|
|
105
|
+
'SampledRequestsEnabled': True,
|
|
106
|
+
'CloudWatchMetricsEnabled': True,
|
|
107
|
+
'MetricName': name
|
|
108
|
+
},
|
|
109
|
+
LockToken=lock_token
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
print(f"WebACL updated: {webacl_arn}")
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
'PhysicalResourceId': webacl_id,
|
|
116
|
+
'Data': {
|
|
117
|
+
'WebAclArn': webacl_arn,
|
|
118
|
+
'WebAclId': webacl_id
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def delete_webacl(webacl_id: str, name: str) -> Dict[str, Any]:
|
|
123
|
+
"""Delete a WAF WebACL."""
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Get current WebACL to get lock token
|
|
127
|
+
response = wafv2.get_web_acl(
|
|
128
|
+
Name=name,
|
|
129
|
+
Scope='CLOUDFRONT',
|
|
130
|
+
Id=webacl_id
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
lock_token = response['LockToken']
|
|
134
|
+
|
|
135
|
+
wafv2.delete_web_acl(
|
|
136
|
+
Name=name,
|
|
137
|
+
Scope='CLOUDFRONT',
|
|
138
|
+
Id=webacl_id,
|
|
139
|
+
LockToken=lock_token
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
print(f"WebACL deleted: {webacl_id}")
|
|
143
|
+
except wafv2.exceptions.WAFNonexistentItemException:
|
|
144
|
+
print(f"WebACL not found: {webacl_id}")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"Error deleting WebACL: {e}")
|
|
147
|
+
import traceback
|
|
148
|
+
traceback.print_exc()
|
|
149
|
+
# Don't raise on delete - best effort cleanup
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
'PhysicalResourceId': webacl_id,
|
|
153
|
+
'Data': {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def build_rules(enable_managed_rules: bool, rate_limit: int, path_rate_limits: List[Dict[str, Any]], allowed_countries: List[str], blocked_countries: List[str]) -> List[Dict[str, Any]]:
|
|
157
|
+
"""Build the rules list for the WebACL."""
|
|
158
|
+
|
|
159
|
+
rules = []
|
|
160
|
+
priority = 0
|
|
161
|
+
|
|
162
|
+
# Geo-blocking rules (highest priority)
|
|
163
|
+
if allowed_countries:
|
|
164
|
+
print(f"Adding geo-allow rule for countries: {allowed_countries}")
|
|
165
|
+
rules.append({
|
|
166
|
+
'Name': 'GeoAllowRule',
|
|
167
|
+
'Priority': priority,
|
|
168
|
+
'Statement': {
|
|
169
|
+
'NotStatement': {
|
|
170
|
+
'Statement': {
|
|
171
|
+
'GeoMatchStatement': {
|
|
172
|
+
'CountryCodes': allowed_countries
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
'Action': {
|
|
178
|
+
'Block': {}
|
|
179
|
+
},
|
|
180
|
+
'VisibilityConfig': {
|
|
181
|
+
'SampledRequestsEnabled': True,
|
|
182
|
+
'CloudWatchMetricsEnabled': True,
|
|
183
|
+
'MetricName': 'GeoAllowRule'
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
priority += 1
|
|
187
|
+
|
|
188
|
+
if blocked_countries:
|
|
189
|
+
print(f"Adding geo-block rule for countries: {blocked_countries}")
|
|
190
|
+
rules.append({
|
|
191
|
+
'Name': 'GeoBlockRule',
|
|
192
|
+
'Priority': priority,
|
|
193
|
+
'Statement': {
|
|
194
|
+
'GeoMatchStatement': {
|
|
195
|
+
'CountryCodes': blocked_countries
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
'Action': {
|
|
199
|
+
'Block': {}
|
|
200
|
+
},
|
|
201
|
+
'VisibilityConfig': {
|
|
202
|
+
'SampledRequestsEnabled': True,
|
|
203
|
+
'CloudWatchMetricsEnabled': True,
|
|
204
|
+
'MetricName': 'GeoBlockRule'
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
priority += 1
|
|
208
|
+
|
|
209
|
+
# Path-specific rate limiting rules (before general rate limit)
|
|
210
|
+
for idx, path_limit in enumerate(path_rate_limits):
|
|
211
|
+
path = path_limit['path']
|
|
212
|
+
limit = int(path_limit['rateLimit'])
|
|
213
|
+
rule_name = path_limit.get('name', f"PathRateLimit{idx}")
|
|
214
|
+
|
|
215
|
+
print(f"Adding path-specific rate limit: {path} = {limit} req/5min")
|
|
216
|
+
|
|
217
|
+
rules.append({
|
|
218
|
+
'Name': rule_name,
|
|
219
|
+
'Priority': priority,
|
|
220
|
+
'Statement': {
|
|
221
|
+
'RateBasedStatement': {
|
|
222
|
+
'Limit': limit,
|
|
223
|
+
'AggregateKeyType': 'IP',
|
|
224
|
+
'ScopeDownStatement': {
|
|
225
|
+
'ByteMatchStatement': {
|
|
226
|
+
'SearchString': path,
|
|
227
|
+
'FieldToMatch': {
|
|
228
|
+
'UriPath': {}
|
|
229
|
+
},
|
|
230
|
+
'TextTransformations': [{
|
|
231
|
+
'Priority': 0,
|
|
232
|
+
'Type': 'NONE'
|
|
233
|
+
}],
|
|
234
|
+
'PositionalConstraint': 'STARTS_WITH' if not '*' in path else 'CONTAINS'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
'Action': {
|
|
240
|
+
'Block': {}
|
|
241
|
+
},
|
|
242
|
+
'VisibilityConfig': {
|
|
243
|
+
'SampledRequestsEnabled': True,
|
|
244
|
+
'CloudWatchMetricsEnabled': True,
|
|
245
|
+
'MetricName': rule_name
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
priority += 1
|
|
249
|
+
|
|
250
|
+
# General rate limiting rule
|
|
251
|
+
rules.append({
|
|
252
|
+
'Name': 'RateLimitRule',
|
|
253
|
+
'Priority': priority,
|
|
254
|
+
'Statement': {
|
|
255
|
+
'RateBasedStatement': {
|
|
256
|
+
'Limit': rate_limit,
|
|
257
|
+
'AggregateKeyType': 'IP'
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
'Action': {
|
|
261
|
+
'Block': {}
|
|
262
|
+
},
|
|
263
|
+
'VisibilityConfig': {
|
|
264
|
+
'SampledRequestsEnabled': True,
|
|
265
|
+
'CloudWatchMetricsEnabled': True,
|
|
266
|
+
'MetricName': 'RateLimitRule'
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
priority += 1
|
|
270
|
+
|
|
271
|
+
if enable_managed_rules:
|
|
272
|
+
# Core Rule Set
|
|
273
|
+
rules.append({
|
|
274
|
+
'Name': 'AWSManagedRulesCommonRuleSet',
|
|
275
|
+
'Priority': priority,
|
|
276
|
+
'Statement': {
|
|
277
|
+
'ManagedRuleGroupStatement': {
|
|
278
|
+
'VendorName': 'AWS',
|
|
279
|
+
'Name': 'AWSManagedRulesCommonRuleSet'
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
'OverrideAction': {
|
|
283
|
+
'None': {}
|
|
284
|
+
},
|
|
285
|
+
'VisibilityConfig': {
|
|
286
|
+
'SampledRequestsEnabled': True,
|
|
287
|
+
'CloudWatchMetricsEnabled': True,
|
|
288
|
+
'MetricName': 'AWSManagedRulesCommonRuleSet'
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
priority += 1
|
|
292
|
+
|
|
293
|
+
# Known Bad Inputs
|
|
294
|
+
rules.append({
|
|
295
|
+
'Name': 'AWSManagedRulesKnownBadInputsRuleSet',
|
|
296
|
+
'Priority': priority,
|
|
297
|
+
'Statement': {
|
|
298
|
+
'ManagedRuleGroupStatement': {
|
|
299
|
+
'VendorName': 'AWS',
|
|
300
|
+
'Name': 'AWSManagedRulesKnownBadInputsRuleSet'
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
'OverrideAction': {
|
|
304
|
+
'None': {}
|
|
305
|
+
},
|
|
306
|
+
'VisibilityConfig': {
|
|
307
|
+
'SampledRequestsEnabled': True,
|
|
308
|
+
'CloudWatchMetricsEnabled': True,
|
|
309
|
+
'MetricName': 'AWSManagedRulesKnownBadInputsRuleSet'
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
priority += 1
|
|
313
|
+
|
|
314
|
+
# Amazon IP Reputation List
|
|
315
|
+
rules.append({
|
|
316
|
+
'Name': 'AWSManagedRulesAmazonIpReputationList',
|
|
317
|
+
'Priority': priority,
|
|
318
|
+
'Statement': {
|
|
319
|
+
'ManagedRuleGroupStatement': {
|
|
320
|
+
'VendorName': 'AWS',
|
|
321
|
+
'Name': 'AWSManagedRulesAmazonIpReputationList'
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
'OverrideAction': {
|
|
325
|
+
'None': {}
|
|
326
|
+
},
|
|
327
|
+
'VisibilityConfig': {
|
|
328
|
+
'SampledRequestsEnabled': True,
|
|
329
|
+
'CloudWatchMetricsEnabled': True,
|
|
330
|
+
'MetricName': 'AWSManagedRulesAmazonIpReputationList'
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
priority += 1
|
|
334
|
+
|
|
335
|
+
# Anonymous IP List
|
|
336
|
+
rules.append({
|
|
337
|
+
'Name': 'AWSManagedRulesAnonymousIpList',
|
|
338
|
+
'Priority': priority,
|
|
339
|
+
'Statement': {
|
|
340
|
+
'ManagedRuleGroupStatement': {
|
|
341
|
+
'VendorName': 'AWS',
|
|
342
|
+
'Name': 'AWSManagedRulesAnonymousIpList'
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
'OverrideAction': {
|
|
346
|
+
'None': {}
|
|
347
|
+
},
|
|
348
|
+
'VisibilityConfig': {
|
|
349
|
+
'SampledRequestsEnabled': True,
|
|
350
|
+
'CloudWatchMetricsEnabled': True,
|
|
351
|
+
'MetricName': 'AWSManagedRulesAnonymousIpList'
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
priority += 1
|
|
355
|
+
|
|
356
|
+
return rules
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Audit Log Archive
|
|
2
|
+
|
|
3
|
+
A reusable CDK construct for archiving CloudWatch Logs to S3 in Parquet format for long-term storage and analytics with Athena.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Parquet Format**: Columnar storage with Snappy compression (80-90% smaller than JSON)
|
|
8
|
+
- **Intelligent-Tiering**: Automatic cost optimization for S3 storage
|
|
9
|
+
- **Athena-Ready**: Pre-configured Glue database and table for immediate querying
|
|
10
|
+
- **Partitioned**: Date-based partitioning (year/month/day) for efficient queries
|
|
11
|
+
- **Encrypted**: KMS encryption for data at rest
|
|
12
|
+
- **Configurable Retention**: Separate retention for CloudWatch Logs and S3 archive
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
CloudWatch Logs → Subscription Filter → Kinesis Firehose → S3 (Parquet)
|
|
18
|
+
↓
|
|
19
|
+
Glue Catalog
|
|
20
|
+
↓
|
|
21
|
+
Athena
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { AuditLogArchive } from './constructs/cloudfront/logging';
|
|
28
|
+
|
|
29
|
+
const auditLogs = new AuditLogArchive(this, 'AuditLogs', {
|
|
30
|
+
logGroupNames: [
|
|
31
|
+
'/aws/lambda/my-function-1',
|
|
32
|
+
'/aws/lambda/my-function-2',
|
|
33
|
+
],
|
|
34
|
+
kmsKey: myKmsKey,
|
|
35
|
+
retentionDays: 30, // CloudWatch Logs retention (default: 30)
|
|
36
|
+
archiveRetentionDays: 365, // S3 archive retention (default: 365)
|
|
37
|
+
bucketName: 'my-audit-logs', // Optional
|
|
38
|
+
databaseName: 'audit_logs', // Optional
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Properties
|
|
43
|
+
|
|
44
|
+
| Property | Type | Required | Default | Description |
|
|
45
|
+
|----------|------|----------|---------|-------------|
|
|
46
|
+
| `logGroupNames` | `string[]` | Yes | - | CloudWatch Log Groups to archive |
|
|
47
|
+
| `kmsKey` | `IKey` | Yes | - | KMS key for encryption |
|
|
48
|
+
| `retentionDays` | `number` | No | 30 | CloudWatch Logs retention in days |
|
|
49
|
+
| `archiveRetentionDays` | `number` | No | 365 | S3 archive retention in days |
|
|
50
|
+
| `bucketName` | `string` | No | Auto-generated | S3 bucket name |
|
|
51
|
+
| `databaseName` | `string` | No | `audit_logs` | Glue database name |
|
|
52
|
+
|
|
53
|
+
## Outputs
|
|
54
|
+
|
|
55
|
+
- `bucket`: S3 Bucket for archive storage
|
|
56
|
+
- `database`: Glue Database for Athena
|
|
57
|
+
- `table`: Glue Table with Parquet schema
|
|
58
|
+
- `deliveryStream`: Kinesis Firehose delivery stream
|
|
59
|
+
|
|
60
|
+
## Querying with Athena
|
|
61
|
+
|
|
62
|
+
### Example Queries
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
-- View all logs from the last 7 days
|
|
66
|
+
SELECT * FROM audit_logs.logs
|
|
67
|
+
WHERE year = '2024' AND month = '12'
|
|
68
|
+
ORDER BY timestamp DESC
|
|
69
|
+
LIMIT 100;
|
|
70
|
+
|
|
71
|
+
-- Count logs by log group
|
|
72
|
+
SELECT log_group, COUNT(*) as count
|
|
73
|
+
FROM audit_logs.logs
|
|
74
|
+
WHERE year = '2024' AND month = '12'
|
|
75
|
+
GROUP BY log_group;
|
|
76
|
+
|
|
77
|
+
-- Search for specific events
|
|
78
|
+
SELECT timestamp, message, log_group
|
|
79
|
+
FROM audit_logs.logs
|
|
80
|
+
WHERE message LIKE '%authentication%'
|
|
81
|
+
AND year = '2024' AND month = '12';
|
|
82
|
+
|
|
83
|
+
-- Find logs for a specific user
|
|
84
|
+
SELECT * FROM audit_logs.logs
|
|
85
|
+
WHERE user_id = 'user@example.com'
|
|
86
|
+
AND year = '2024' AND month = '12'
|
|
87
|
+
ORDER BY timestamp DESC;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Schema
|
|
91
|
+
|
|
92
|
+
| Column | Type | Description |
|
|
93
|
+
|--------|------|-------------|
|
|
94
|
+
| `timestamp` | bigint | Log timestamp in milliseconds |
|
|
95
|
+
| `message` | string | Log message |
|
|
96
|
+
| `log_group` | string | CloudWatch log group name |
|
|
97
|
+
| `log_stream` | string | CloudWatch log stream name |
|
|
98
|
+
| `event_type` | string | Event type (extracted from message) |
|
|
99
|
+
| `user_id` | string | User identifier (if available) |
|
|
100
|
+
| `ip_address` | string | Client IP address (if available) |
|
|
101
|
+
|
|
102
|
+
**Partition Keys**: `year`, `month`, `day`
|
|
103
|
+
|
|
104
|
+
## Cost Optimization
|
|
105
|
+
|
|
106
|
+
### Storage Costs (per GB/month)
|
|
107
|
+
- CloudWatch Logs: $0.50
|
|
108
|
+
- S3 Standard: $0.023
|
|
109
|
+
- S3 Intelligent-Tiering (90+ days): $0.0125
|
|
110
|
+
- S3 Intelligent-Tiering (180+ days): $0.004
|
|
111
|
+
|
|
112
|
+
### Query Costs
|
|
113
|
+
- Athena: $5 per TB scanned
|
|
114
|
+
- Parquet reduces scan size by 80-90% vs JSON
|
|
115
|
+
|
|
116
|
+
### Example Cost (1TB logs/year)
|
|
117
|
+
- CloudWatch (30 days): ~$15/month
|
|
118
|
+
- S3 Archive (365 days): ~$5-10/month
|
|
119
|
+
- Athena queries: ~$0.50 per TB scanned (vs $5 for JSON)
|
|
120
|
+
|
|
121
|
+
**Total**: ~$20-25/month for 1TB of logs with full year retention
|
|
122
|
+
|
|
123
|
+
## Best Practices
|
|
124
|
+
|
|
125
|
+
1. **Use Partitions**: Always filter by year/month/day in queries
|
|
126
|
+
2. **Limit Columns**: Select only needed columns to reduce scan size
|
|
127
|
+
3. **Batch Queries**: Combine multiple queries to reduce overhead
|
|
128
|
+
4. **Monitor Costs**: Use AWS Cost Explorer to track Athena usage
|
|
129
|
+
5. **Lifecycle Policies**: Adjust retention based on compliance requirements
|
|
130
|
+
|
|
131
|
+
## Integration with CloudFront Auth
|
|
132
|
+
|
|
133
|
+
The `CloudFrontWithAzureAuth` construct automatically creates an `AuditLogArchive` instance for all authentication-related Lambda functions:
|
|
134
|
+
|
|
135
|
+
- OAuth callback logs
|
|
136
|
+
- Secret rotation logs
|
|
137
|
+
- Session revocation logs
|
|
138
|
+
- Stream processor logs
|
|
139
|
+
|
|
140
|
+
Access via:
|
|
141
|
+
```typescript
|
|
142
|
+
const authConstruct = new CloudFrontWithAzureAuth(this, 'Auth', { ... });
|
|
143
|
+
const auditBucket = authConstruct.auditLogArchive?.bucket;
|
|
144
|
+
```
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Split-Stack CloudFront Auth Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This architecture splits CloudFront authentication resources across two regions:
|
|
6
|
+
- **Regional Stack (ap-southeast-2)**: DynamoDB, Secrets Manager, KMS, Lambda functions, audit logging
|
|
7
|
+
- **CloudFront Stack (us-east-1)**: CloudFront Distribution, Lambda@Edge, CloudFront Functions, ACM Certificate
|
|
8
|
+
|
|
9
|
+
## Benefits
|
|
10
|
+
|
|
11
|
+
- Lower latency for ANZ traffic (DynamoDB in ap-southeast-2)
|
|
12
|
+
- Reduced us-east-1 footprint
|
|
13
|
+
- Better data locality
|
|
14
|
+
- Regional Lambda functions closer to data
|
|
15
|
+
|
|
16
|
+
## Usage Example
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import * as core from 'aws-cdk-lib';
|
|
20
|
+
import { aws_s3 as s3, aws_cloudfront_origins as origins } from 'aws-cdk-lib';
|
|
21
|
+
import * as local from '../constructs';
|
|
22
|
+
|
|
23
|
+
// Regional Stack (ap-southeast-2)
|
|
24
|
+
export class AuthRegionalStack extends core.Stack {
|
|
25
|
+
public readonly authInfra: local.cloudfront.patterns.AuthInfrastructure;
|
|
26
|
+
|
|
27
|
+
constructor(scope: core.App, id: string, props: core.StackProps) {
|
|
28
|
+
super(scope, id, props);
|
|
29
|
+
|
|
30
|
+
this.authInfra = new local.cloudfront.patterns.AuthInfrastructure(this, 'AuthInfra', {
|
|
31
|
+
domainNames: ['bicep-cdk.raindancers.cloud'],
|
|
32
|
+
redirectUri: 'https://bicep-cdk.raindancers.cloud/oauth2/callback',
|
|
33
|
+
hmacSecretRotationSchedule: core.Duration.hours(12),
|
|
34
|
+
autoRevokeOnReuse: true,
|
|
35
|
+
auditLogRetentionDays: 30,
|
|
36
|
+
auditArchiveRetentionDays: 365,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// CloudFront Stack (us-east-1)
|
|
42
|
+
export class CloudFrontStack extends core.Stack {
|
|
43
|
+
constructor(
|
|
44
|
+
scope: core.App,
|
|
45
|
+
id: string,
|
|
46
|
+
props: core.StackProps & {
|
|
47
|
+
regionalInfra: local.cloudfront.patterns.AuthInfrastructure;
|
|
48
|
+
jwtDecoderUrl: string;
|
|
49
|
+
}
|
|
50
|
+
) {
|
|
51
|
+
super(scope, id, { ...props, crossRegionReferences: true });
|
|
52
|
+
|
|
53
|
+
const zone = new local.route53.PublicHostedZone(this, 'Zone', {
|
|
54
|
+
zoneName: 'bicep-cdk.raindancers.cloud',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const cert = new local.cloudfront.CloudFrontCertificate(this, 'Certificate', {
|
|
58
|
+
domainName: 'bicep-cdk.raindancers.cloud',
|
|
59
|
+
hostedZone: zone,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const webAcl = new local.cloudfront.CloudFrontWebAcl(this, 'WebAcl', {
|
|
63
|
+
rateLimit: 10000,
|
|
64
|
+
enableManagedRules: true,
|
|
65
|
+
allowedCountries: ['NewZealand', 'Australia'],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const bucket = new s3.Bucket(this, 'ContentBucket', {
|
|
69
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
70
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const authDistribution = new local.cloudfront.patterns.CloudFrontWithAzureAuthSplit(
|
|
74
|
+
this,
|
|
75
|
+
'AuthDistribution',
|
|
76
|
+
{
|
|
77
|
+
origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
|
|
78
|
+
domainNames: ['bicep-cdk.raindancers.cloud'],
|
|
79
|
+
certificate: cert.certificate,
|
|
80
|
+
webAclId: webAcl.webAclArn,
|
|
81
|
+
jwtDecoderUrl: props.jwtDecoderUrl,
|
|
82
|
+
regionalInfrastructure: props.regionalInfra,
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
new local.route53.CloudFrontAliasRecords(this, 'AliasRecords', {
|
|
87
|
+
zone: zone,
|
|
88
|
+
distribution: authDistribution.distribution,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// App
|
|
94
|
+
const app = new core.App();
|
|
95
|
+
|
|
96
|
+
const regionalStack = new AuthRegionalStack(app, 'AuthRegional', {
|
|
97
|
+
env: { region: 'ap-southeast-2', account: process.env.CDK_DEFAULT_ACCOUNT },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const cloudFrontStack = new CloudFrontStack(app, 'CloudFront', {
|
|
101
|
+
env: { region: 'us-east-1', account: process.env.CDK_DEFAULT_ACCOUNT },
|
|
102
|
+
crossRegionReferences: true,
|
|
103
|
+
regionalInfra: regionalStack.authInfra,
|
|
104
|
+
jwtDecoderUrl: 'https://example.com/jwt',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
cloudFrontStack.addDependency(regionalStack);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Migration from Single Stack
|
|
111
|
+
|
|
112
|
+
If you're currently using `CloudFrontWithAzureAuth`:
|
|
113
|
+
|
|
114
|
+
1. Create new regional stack with `AuthInfrastructure`
|
|
115
|
+
2. Create new CloudFront stack with `CloudFrontWithAzureAuthSplit`
|
|
116
|
+
3. Deploy regional stack first
|
|
117
|
+
4. Deploy CloudFront stack
|
|
118
|
+
5. Update DNS
|
|
119
|
+
6. Delete old single-stack resources
|
|
120
|
+
|
|
121
|
+
## Resource Locations
|
|
122
|
+
|
|
123
|
+
### ap-southeast-2
|
|
124
|
+
- DynamoDB Table
|
|
125
|
+
- Secrets Manager Secret
|
|
126
|
+
- KMS Key
|
|
127
|
+
- Lambda Functions (copy, rotate, stream processor, revocation)
|
|
128
|
+
- CloudWatch Log Groups
|
|
129
|
+
- S3 Audit Archive Bucket
|
|
130
|
+
- IAM Role (created here, used in us-east-1)
|
|
131
|
+
|
|
132
|
+
### us-east-1
|
|
133
|
+
- CloudFront Distribution
|
|
134
|
+
- CloudFront Function
|
|
135
|
+
- Lambda@Edge Function
|
|
136
|
+
- CloudFront KeyValueStore
|
|
137
|
+
- ACM Certificate
|
|
138
|
+
- WAF WebACL
|