cdk-factory 0.13.0__py3-none-any.whl → 0.13.1__py3-none-any.whl
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.
Potentially problematic release.
This version of cdk-factory might be problematic. Click here for more details.
- cdk_factory/lambdas/edge/ip_gate/handler.py +163 -80
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +15 -1
- cdk_factory/version.py +1 -1
- {cdk_factory-0.13.0.dist-info → cdk_factory-0.13.1.dist-info}/METADATA +1 -1
- {cdk_factory-0.13.0.dist-info → cdk_factory-0.13.1.dist-info}/RECORD +8 -8
- {cdk_factory-0.13.0.dist-info → cdk_factory-0.13.1.dist-info}/WHEEL +0 -0
- {cdk_factory-0.13.0.dist-info → cdk_factory-0.13.1.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.13.0.dist-info → cdk_factory-0.13.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,104 +1,187 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Lambda@Edge
|
|
2
|
+
Lambda@Edge function for IP-based access gating.
|
|
3
3
|
Geek Cafe, LLC
|
|
4
4
|
Maintainers: Eric Wilson
|
|
5
|
-
"""
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
Since Lambda@Edge does not support environment variables, configuration
|
|
7
|
+
is fetched from SSM Parameter Store at runtime (with caching).
|
|
8
|
+
"""
|
|
8
9
|
import json
|
|
9
|
-
import
|
|
10
|
+
import ipaddress
|
|
11
|
+
import boto3
|
|
12
|
+
from functools import lru_cache
|
|
10
13
|
|
|
14
|
+
# SSM client - will be created in the region where the function executes
|
|
15
|
+
ssm = None
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
@lru_cache(maxsize=128)
|
|
18
|
+
def get_ssm_parameter(parameter_name: str, region: str = 'us-east-1') -> str:
|
|
13
19
|
"""
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
Fetch SSM parameter with caching.
|
|
21
|
+
Lambda@Edge cannot use environment variables, so we fetch from SSM.
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- Rewrite blocked IPs to maintenance CloudFront distribution, and serve up maintenance site
|
|
21
|
-
- Toggle via GATE_ENABLED environment variable
|
|
22
|
-
"""
|
|
23
|
+
Args:
|
|
24
|
+
parameter_name: Name of the SSM parameter
|
|
25
|
+
region: AWS region (default us-east-1)
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
Returns:
|
|
28
|
+
Parameter value
|
|
29
|
+
"""
|
|
30
|
+
global ssm
|
|
31
|
+
if ssm is None:
|
|
32
|
+
ssm = boto3.client('ssm', region_name=region)
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
try:
|
|
35
|
+
response = ssm.get_parameter(Name=parameter_name, WithDecryption=False)
|
|
36
|
+
return response['Parameter']['Value']
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(f"Error fetching SSM parameter {parameter_name}: {str(e)}")
|
|
39
|
+
raise
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_client_ip(request):
|
|
43
|
+
"""Extract client IP from CloudFront request."""
|
|
44
|
+
if 'clientIp' in request:
|
|
45
|
+
return request['clientIp']
|
|
32
46
|
|
|
33
|
-
#
|
|
34
|
-
|
|
47
|
+
# Fallback to headers
|
|
48
|
+
headers = request.get('headers', {})
|
|
49
|
+
if 'x-forwarded-for' in headers:
|
|
50
|
+
xff = headers['x-forwarded-for'][0]['value']
|
|
51
|
+
return xff.split(',')[0].strip()
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_ip_allowed(client_ip: str, allowed_cidrs: list) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if client IP is in any of the allowed CIDR ranges.
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
'
|
|
43
|
-
}]
|
|
60
|
+
Args:
|
|
61
|
+
client_ip: Client IP address
|
|
62
|
+
allowed_cidrs: List of CIDR ranges (e.g., ['10.0.0.0/8', '192.168.1.0/24'])
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
Returns:
|
|
65
|
+
True if IP is allowed, False otherwise
|
|
66
|
+
"""
|
|
67
|
+
if not client_ip:
|
|
68
|
+
return False
|
|
48
69
|
|
|
49
|
-
# Check if IP is in allowlist
|
|
50
|
-
ip_allowed = False
|
|
51
70
|
try:
|
|
52
71
|
client_ip_obj = ipaddress.ip_address(client_ip)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if client_ip_obj in network:
|
|
57
|
-
ip_allowed = True
|
|
58
|
-
break
|
|
59
|
-
except (ValueError, ipaddress.AddressValueError):
|
|
60
|
-
# Invalid CIDR, skip
|
|
61
|
-
continue
|
|
62
|
-
except (ValueError, ipaddress.AddressValueError):
|
|
63
|
-
# Invalid client IP, block by default
|
|
64
|
-
ip_allowed = False
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
print(f"Invalid client IP address: {e}")
|
|
74
|
+
return False
|
|
65
75
|
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
# Check each CIDR individually, skipping invalid ones
|
|
77
|
+
for cidr in allowed_cidrs:
|
|
78
|
+
try:
|
|
79
|
+
network = ipaddress.ip_network(cidr.strip(), strict=False)
|
|
80
|
+
if client_ip_obj in network:
|
|
81
|
+
return True
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
# Invalid CIDR, log and continue checking others
|
|
84
|
+
print(f"Invalid CIDR '{cidr}': {e}")
|
|
85
|
+
continue
|
|
69
86
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def lambda_handler(event, context):
|
|
91
|
+
"""
|
|
92
|
+
Lambda@Edge function for IP-based gating.
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'port': 443,
|
|
81
|
-
'protocol': 'https',
|
|
82
|
-
'path': '',
|
|
83
|
-
'sslProtocols': ['TLSv1.2'],
|
|
84
|
-
'readTimeout': 30,
|
|
85
|
-
'keepaliveTimeout': 5,
|
|
86
|
-
'customHeaders': {}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
94
|
+
Configuration (fetched from SSM Parameter Store):
|
|
95
|
+
- GATE_ENABLED: Whether IP gating is enabled (true/false)
|
|
96
|
+
- ALLOW_CIDRS: Comma-separated list of allowed CIDR ranges
|
|
97
|
+
- MAINT_CF_HOST: CloudFront domain for maintenance/lockout page
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
Runtime configuration is bundled in runtime_config.json by CDK.
|
|
100
|
+
SSM parameter paths are auto-generated by CDK as:
|
|
101
|
+
/{environment}/{full-function-name}/{env-var-name-kebab-case}
|
|
102
|
+
"""
|
|
103
|
+
request = event['Records'][0]['cf']['request']
|
|
104
|
+
|
|
105
|
+
# Load runtime configuration bundled by CDK
|
|
106
|
+
# This file is created during deployment and contains environment, function name, etc.
|
|
107
|
+
try:
|
|
108
|
+
with open('runtime_config.json', 'r') as f:
|
|
109
|
+
runtime_config = json.load(f)
|
|
110
|
+
|
|
111
|
+
env = runtime_config.get('environment', 'dev')
|
|
112
|
+
function_base_name = runtime_config.get('function_name', 'ip-gate')
|
|
113
|
+
|
|
114
|
+
print(f"Runtime config loaded: environment={env}, function_name={function_base_name}")
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
# Fallback: extract from Lambda context (less reliable)
|
|
117
|
+
print("Warning: runtime_config.json not found, falling back to function name parsing")
|
|
118
|
+
function_full_name = context.function_name
|
|
119
|
+
|
|
120
|
+
# Parse environment from function name as fallback
|
|
121
|
+
parts = function_full_name.split('-')
|
|
122
|
+
common_envs = ['dev', 'prod', 'staging', 'test', 'qa', 'uat']
|
|
123
|
+
env = 'dev'
|
|
124
|
+
|
|
125
|
+
for part in parts:
|
|
126
|
+
if part in common_envs:
|
|
127
|
+
env = part
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
function_base_name = 'ip-gate'
|
|
131
|
+
print(f"Fallback: environment={env}, function_name={function_base_name}")
|
|
95
132
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
request['uri'] = uri + 'index.html'
|
|
100
|
-
elif '.' not in uri.split('/')[-1]:
|
|
101
|
-
# No file extension, likely a directory
|
|
102
|
-
request['uri'] = uri + '/index.html'
|
|
133
|
+
# Full function name for SSM paths
|
|
134
|
+
function_name = context.function_name
|
|
135
|
+
print(f"Lambda function ARN: {context.invoked_function_arn}")
|
|
103
136
|
|
|
104
|
-
|
|
137
|
+
try:
|
|
138
|
+
# Fetch configuration from SSM Parameter Store
|
|
139
|
+
# Auto-generated paths: /{env}/{function-name}/{key}
|
|
140
|
+
gate_enabled = get_ssm_parameter(f'/{env}/{function_name}/gate-enabled', 'us-east-1')
|
|
141
|
+
|
|
142
|
+
# If gating is disabled, allow all traffic
|
|
143
|
+
if gate_enabled.lower() not in ('true', '1', 'yes'):
|
|
144
|
+
print(f"IP gating is disabled (GATE_ENABLED={gate_enabled})")
|
|
145
|
+
return request
|
|
146
|
+
|
|
147
|
+
# Get allowed CIDRs and maintenance host
|
|
148
|
+
allow_cidrs_str = get_ssm_parameter(f'/{env}/{function_name}/allow-cidrs', 'us-east-1')
|
|
149
|
+
maint_cf_host = get_ssm_parameter(f'/{env}/{function_name}/maint-cf-host', 'us-east-1')
|
|
150
|
+
|
|
151
|
+
# Parse allowed CIDRs
|
|
152
|
+
allowed_cidrs = [cidr.strip() for cidr in allow_cidrs_str.split(',') if cidr.strip()]
|
|
153
|
+
|
|
154
|
+
# Get client IP
|
|
155
|
+
client_ip = get_client_ip(request)
|
|
156
|
+
print(f"Client IP: {client_ip}")
|
|
157
|
+
|
|
158
|
+
# Check if IP is allowed
|
|
159
|
+
if is_ip_allowed(client_ip, allowed_cidrs):
|
|
160
|
+
print(f"IP {client_ip} is allowed")
|
|
161
|
+
return request
|
|
162
|
+
|
|
163
|
+
# IP not allowed - redirect to maintenance page
|
|
164
|
+
print(f"IP {client_ip} is NOT allowed, redirecting to {maint_cf_host}")
|
|
165
|
+
|
|
166
|
+
response = {
|
|
167
|
+
'status': '302',
|
|
168
|
+
'statusDescription': 'Found',
|
|
169
|
+
'headers': {
|
|
170
|
+
'location': [{
|
|
171
|
+
'key': 'Location',
|
|
172
|
+
'value': f'https://{maint_cf_host}'
|
|
173
|
+
}],
|
|
174
|
+
'cache-control': [{
|
|
175
|
+
'key': 'Cache-Control',
|
|
176
|
+
'value': 'no-cache, no-store, must-revalidate'
|
|
177
|
+
}]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return response
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"Error in IP gating function: {str(e)}")
|
|
185
|
+
# On error, allow the request to proceed (fail open)
|
|
186
|
+
# Change this to fail closed if preferred
|
|
187
|
+
return request
|
|
@@ -9,6 +9,8 @@ MIT License. See Project Root for the license information.
|
|
|
9
9
|
from typing import Optional, Dict
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
import json
|
|
12
|
+
import tempfile
|
|
13
|
+
import shutil
|
|
12
14
|
|
|
13
15
|
import aws_cdk as cdk
|
|
14
16
|
from aws_cdk import aws_lambda as _lambda
|
|
@@ -140,6 +142,15 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
|
|
|
140
142
|
|
|
141
143
|
logger.info(f"Loading Lambda code from: {code_path}")
|
|
142
144
|
|
|
145
|
+
# Create isolated temp directory for this function instance
|
|
146
|
+
# This prevents conflicts when multiple functions use the same handler code
|
|
147
|
+
temp_code_dir = Path(tempfile.mkdtemp(prefix=f"{function_name.replace('/', '-')}-"))
|
|
148
|
+
logger.info(f"Creating isolated code directory at: {temp_code_dir}")
|
|
149
|
+
|
|
150
|
+
# Copy source code to temp directory
|
|
151
|
+
shutil.copytree(code_path, temp_code_dir, dirs_exist_ok=True)
|
|
152
|
+
logger.info(f"Copied code from {code_path} to {temp_code_dir}")
|
|
153
|
+
|
|
143
154
|
# Create runtime configuration file for Lambda@Edge
|
|
144
155
|
# Since Lambda@Edge doesn't support environment variables, we bundle a config file
|
|
145
156
|
runtime_config = {
|
|
@@ -148,7 +159,7 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
|
|
|
148
159
|
'region': self.deployment.region
|
|
149
160
|
}
|
|
150
161
|
|
|
151
|
-
runtime_config_path =
|
|
162
|
+
runtime_config_path = temp_code_dir / 'runtime_config.json'
|
|
152
163
|
logger.info(f"Creating runtime config at: {runtime_config_path}")
|
|
153
164
|
|
|
154
165
|
with open(runtime_config_path, 'w') as f:
|
|
@@ -156,6 +167,9 @@ class LambdaEdgeStack(IStack, EnhancedSsmParameterMixin):
|
|
|
156
167
|
|
|
157
168
|
logger.info(f"Runtime config: {runtime_config}")
|
|
158
169
|
|
|
170
|
+
# Use the temp directory for the Lambda code asset
|
|
171
|
+
code_path = temp_code_dir
|
|
172
|
+
|
|
159
173
|
# Map runtime string to CDK Runtime
|
|
160
174
|
runtime_map = {
|
|
161
175
|
"python3.11": _lambda.Runtime.PYTHON_3_11,
|
cdk_factory/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.13.
|
|
1
|
+
__version__ = "0.13.1"
|
|
@@ -2,7 +2,7 @@ cdk_factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
cdk_factory/app.py,sha256=RnX0-pwdTAPAdKJK_j13Zl8anf9zYKBwboR0KA8K8xM,10346
|
|
3
3
|
cdk_factory/cdk.json,sha256=SKZKhJ2PBpFH78j-F8S3VDYW-lf76--Q2I3ON-ZIQfw,3106
|
|
4
4
|
cdk_factory/cli.py,sha256=FGbCTS5dYCNsfp-etshzvFlGDCjC28r6rtzYbe7KoHI,6407
|
|
5
|
-
cdk_factory/version.py,sha256=
|
|
5
|
+
cdk_factory/version.py,sha256=Zg3oo58_HXe_ieb_PwWnYkKGH2zTvu6G2jly-7GnPGo,23
|
|
6
6
|
cdk_factory/builds/README.md,sha256=9BBWd7bXpyKdMU_g2UljhQwrC9i5O_Tvkb6oPvndoZk,90
|
|
7
7
|
cdk_factory/commands/command_loader.py,sha256=QbLquuP_AdxtlxlDy-2IWCQ6D-7qa58aphnDPtp_uTs,3744
|
|
8
8
|
cdk_factory/configurations/base_config.py,sha256=JKjhNsy0RCUZy1s8n5D_aXXI-upR9izaLtCTfKYiV9k,9624
|
|
@@ -66,7 +66,7 @@ cdk_factory/interfaces/istack.py,sha256=bhTBs-o9FgKwvJMSuwxjUV6D3nUlvZHVzfm27jP9
|
|
|
66
66
|
cdk_factory/interfaces/live_ssm_resolver.py,sha256=3FIr9a02SXqZmbFs3RT0WxczWEQR_CF7QSt7kWbDrVE,8163
|
|
67
67
|
cdk_factory/interfaces/ssm_parameter_mixin.py,sha256=uA2j8HmAOpuEA9ynRj51s0WjUHMVLsbLQN-QS9NKyHA,12089
|
|
68
68
|
cdk_factory/lambdas/health_handler.py,sha256=dd40ykKMxWCFEIyp2ZdQvAGNjw_ylI9CSm1N24Hp2ME,196
|
|
69
|
-
cdk_factory/lambdas/edge/ip_gate/handler.py,sha256=
|
|
69
|
+
cdk_factory/lambdas/edge/ip_gate/handler.py,sha256=NCa16_B66zIczPb9whf_CaspnetuTYKTFpIX6Z7Wqjw,6393
|
|
70
70
|
cdk_factory/pipeline/path_utils.py,sha256=fvWdrcb4onmpIu1APkHLhXg8zWfK74HcW3Ra2ynxfXM,2586
|
|
71
71
|
cdk_factory/pipeline/pipeline_factory.py,sha256=rvtkdlTPJG477nTVRN8S2ksWt4bwpd9eVLFd9WO02pM,17248
|
|
72
72
|
cdk_factory/pipeline/stage.py,sha256=Be7ExMB9A-linRM18IQDOzQ-cP_I2_ThRNzlT4FIrUg,437
|
|
@@ -95,7 +95,7 @@ cdk_factory/stack_library/ecr/ecr_stack.py,sha256=1xA68sxFVyqreYjXrP_7U9I8RF9RtF
|
|
|
95
95
|
cdk_factory/stack_library/ecs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
96
96
|
cdk_factory/stack_library/ecs/ecs_service_stack.py,sha256=zuGdZEP5KmeVDTJb-H47LYhvs-85-Fi4Xb78nsA-lF4,24685
|
|
97
97
|
cdk_factory/stack_library/lambda_edge/__init__.py,sha256=ByBJ_CWdc4UtTmFBZH-6pzBMNkjkdtE65AmnB0Fs6lM,156
|
|
98
|
-
cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py,sha256=
|
|
98
|
+
cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py,sha256=5MXYXFLVpLX2DNDU9doRfUciyS8h1KM6YdpiiEAkMqY,14141
|
|
99
99
|
cdk_factory/stack_library/load_balancer/__init__.py,sha256=wZpKw2OecLJGdF5mPayCYAEhu2H3c2gJFFIxwXftGDU,52
|
|
100
100
|
cdk_factory/stack_library/load_balancer/load_balancer_stack.py,sha256=t5JUe5lMUbQCRFZR08k8nO-g-53yWY8gKB9v8ZnedBs,24391
|
|
101
101
|
cdk_factory/stack_library/monitoring/__init__.py,sha256=k1G_KDx47Aw0UugaL99PN_TKlyLK4nkJVApCaAK7GJg,153
|
|
@@ -129,8 +129,8 @@ cdk_factory/utilities/lambda_function_utilities.py,sha256=S1GvBsY_q2cyUiaud3HORJ
|
|
|
129
129
|
cdk_factory/utilities/os_execute.py,sha256=5Op0LY_8Y-pUm04y1k8MTpNrmQvcLmQHPQITEP7EuSU,1019
|
|
130
130
|
cdk_factory/utils/api_gateway_utilities.py,sha256=If7Xu5s_UxmuV-kL3JkXxPLBdSVUKoLtohm0IUFoiV8,4378
|
|
131
131
|
cdk_factory/workload/workload_factory.py,sha256=mM8GU_5mKq_0OyK060T3JrUSUiGAcKf0eqNlT9mfaws,6028
|
|
132
|
-
cdk_factory-0.13.
|
|
133
|
-
cdk_factory-0.13.
|
|
134
|
-
cdk_factory-0.13.
|
|
135
|
-
cdk_factory-0.13.
|
|
136
|
-
cdk_factory-0.13.
|
|
132
|
+
cdk_factory-0.13.1.dist-info/METADATA,sha256=GC7Guc1LQ_wDzflKXg8VUxUj7ra97GYDF66Ugz3p0eE,2451
|
|
133
|
+
cdk_factory-0.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
134
|
+
cdk_factory-0.13.1.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
|
|
135
|
+
cdk_factory-0.13.1.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
|
|
136
|
+
cdk_factory-0.13.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|