Skip to content

Commit c94e9ab

Browse files
authored
amq broker and ssm secure parameter resources (#6)
* amq broker custom resource * generate and store secure ssm parameter * include new custom resources * syntax fixes * amq broker custom resource * generate and store secure ssm parameter * include new custom resources * syntax fixes
1 parent 2a71bf2 commit c94e9ab

File tree

10 files changed

+507
-0
lines changed

10 files changed

+507
-0
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,105 @@ Required parameters:
6565
- `Data` - Data such as when the value of Type is HEADER , enter the name of the header that you want AWS WAF to search, for example, User-Agent or Referer
6666
- `Transform` - Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass AWS WAF.
6767
Implementation require to be serialised with other waf condition.
68+
### AmazonMQ Broker
69+
70+
This custom resource creates a AmazonMQ broker instance.
71+
72+
**NOTE:** This resource cannot be updated. If a change to the instance is required such as Instance Type, a new broker resource must be created.
73+
74+
handler: `amazon-mq-broker/handler.lambda_handler`
75+
runtime: `python3.6`
76+
77+
Required parameters:
78+
79+
- `Name` - Unique name given to the broker
80+
- `SecurityGroups` - Array of security group ids
81+
- `Subnets` - Array of subnets ids
82+
- `MultiAZ` - String boolean [ 'true', 'false' ]
83+
- `InstanceType` - valid values [ 'mq.t2.micro', 'mq.m4.large' ]
84+
- `Username` - Username for the amq user
85+
- `Password` - Password for the amq user. Must be 12-250 characters long
86+
87+
No optional parameters.
88+
89+
Returned Values:
90+
91+
- `Active` - Active AmazonMQ endpoint
92+
- `Stanby` - Standby AmazonMQ endpoint
93+
- `BrokerId` - Id of the AmazonMQ Broker
94+
- `BrokerArn` - Arn of the broker
95+
96+
IAM Permissions:
97+
98+
```json
99+
{
100+
"Statement":
101+
[
102+
{
103+
"Effect": "Allow",
104+
"Action":
105+
[
106+
"mq:*",
107+
"ec2:CreateNetworkInterface",
108+
"ec2:CreateNetworkInterfacePermission",
109+
"ec2:DeleteNetworkInterface",
110+
"ec2:DeleteNetworkInterfacePermission",
111+
"ec2:DetachNetworkInterface",
112+
"ec2:DescribeInternetGateways",
113+
"ec2:DescribeNetworkInterfaces",
114+
"ec2:DescribeNetworkInterfacePermissions",
115+
"ec2:DescribeRouteTables",
116+
"ec2:DescribeSecurityGroups",
117+
"ec2:DescribeSubnets",
118+
"ec2:DescribeVpcs",
119+
"logs:CreateLogGroup",
120+
"logs:CreateLogStream",
121+
"logs:PutLogEvents",
122+
"lambda:InvokeFunction"
123+
],
124+
"Resource": ["*"]
125+
}
126+
]
127+
}
128+
```
129+
130+
### Auto generated secure ssm parameters
131+
132+
This custom resource generates a random string `[a-z][A-Z][0-9]` a definable length. The string is then return to the cfn stack and can then be passed into other resources requiring a password. The resource can be updated generating a new password and updating the SSM parameter and returning the new password by passing a dummy parameter into the custom resource.
133+
134+
handler: `ssm-secure-parameter/handler.lambda_handler`
135+
runtime: `python3.6`
136+
137+
Required parameters:
138+
139+
- `Path` - SSM parameter path e.g. `/app/env/password`
140+
141+
Optional parameters:
142+
143+
- `Length` - Length of the auto generated password. Defaults to 16 characters
144+
145+
Returned Values:
146+
147+
- `Password` - The password generated by the resource
148+
149+
IAM Permissions:
150+
151+
```json
152+
{
153+
"Statement":
154+
[
155+
{
156+
"Effect": "Allow",
157+
"Action":
158+
[
159+
"ssm:PutParameter",
160+
"ssm:DeleteParameter",
161+
"logs:CreateLogGroup",
162+
"logs:CreateLogStream",
163+
"logs:PutLogEvents"
164+
],
165+
"Resource": ["*"]
166+
}
167+
]
168+
}
169+
```

amazon-mq-broker/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# package marker

amazon-mq-broker/cr_response.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import logging
2+
from urllib.request import urlopen, Request, HTTPError, URLError
3+
import json
4+
5+
logger = logging.getLogger()
6+
logger.setLevel(logging.INFO)
7+
8+
9+
class CustomResourceResponse:
10+
def __init__(self, request_payload):
11+
self.payload = request_payload
12+
self.response = {
13+
"StackId": request_payload["StackId"],
14+
"RequestId": request_payload["RequestId"],
15+
"LogicalResourceId": request_payload["LogicalResourceId"],
16+
"Status": 'SUCCESS',
17+
}
18+
19+
def respond_error(self, message):
20+
self.response['Status'] = 'FAILED'
21+
self.response['Reason'] = message
22+
self.respond()
23+
24+
def respond(self, data=None):
25+
event = self.payload
26+
response = self.response
27+
####
28+
#### copied from https://github.com/ryansb/cfn-wrapper-python/blob/master/cfn_resource.py
29+
####
30+
31+
if event.get("PhysicalResourceId", False):
32+
response["PhysicalResourceId"] = event["PhysicalResourceId"]
33+
34+
if data is not None:
35+
response['Data'] = data
36+
37+
logger.debug("Received %s request with event: %s" % (event['RequestType'], json.dumps(event)))
38+
39+
serialized = json.dumps(response)
40+
logger.info(f"Responding to {event['RequestType']} request with: {serialized}")
41+
42+
req_data = serialized.encode('utf-8')
43+
44+
req = Request(
45+
event['ResponseURL'],
46+
data=req_data,
47+
headers={'Content-Length': len(req_data),'Content-Type': ''}
48+
)
49+
req.get_method = lambda: 'PUT'
50+
51+
try:
52+
urlopen(req)
53+
logger.debug("Request to CFN API succeeded, nothing to do here")
54+
except HTTPError as e:
55+
logger.error("Callback to CFN API failed with status %d" % e.code)
56+
logger.error("Response: %s" % e.reason)
57+
except URLError as e:
58+
logger.error("Failed to reach the server - %s" % e.reason)

amazon-mq-broker/handler.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import sys
2+
import os
3+
import json
4+
5+
sys.path.append(f"{os.environ['LAMBDA_TASK_ROOT']}/lib")
6+
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
7+
8+
import cr_response
9+
import logic
10+
import lambda_invoker
11+
12+
def lambda_handler(event, context):
13+
14+
print(f"Received event:{json.dumps(event)}")
15+
16+
lambda_response = cr_response.CustomResourceResponse(event)
17+
cr_params = event['ResourceProperties']
18+
19+
# Validate input
20+
for key in ['MultiAZ', 'InstanceType', 'Username', 'Password', 'SecurityGroups', 'Subnets']:
21+
if key not in cr_params:
22+
lambda_response.respond_error(f"{key} property missing")
23+
return
24+
25+
try:
26+
broker = logic.AmazonMQBrokerLogic(cr_params['Name'])
27+
if event['RequestType'] == 'Create':
28+
if ('WaitComplete' in event) and (event['WaitComplete']):
29+
result = broker.wait_broker_status(event['PhysicalResourceId'], context)
30+
31+
if result is None:
32+
invoke = lambda_invoker.LambdaInvoker()
33+
invoke.invoke(event)
34+
elif result:
35+
lambda_response.respond(data=event['Data'])
36+
elif not result:
37+
lambda_response.respond_error(f"Creation of AmazonMQ {event['PhysicalResourceId']} failed.")
38+
39+
else:
40+
response = broker.create(
41+
multi_az=cr_params['MultiAZ'],
42+
instance_type=cr_params['InstanceType'],
43+
user=cr_params['Username'],
44+
password=cr_params['Password'],
45+
security_groups=cr_params['SecurityGroups'],
46+
subnets=cr_params['Subnets']
47+
)
48+
49+
event['PhysicalResourceId'] = response['BrokerId']
50+
event['Data'] = response
51+
event['WaitComplete'] = True
52+
invoke = lambda_invoker.LambdaInvoker()
53+
invoke.invoke(event)
54+
55+
elif event['RequestType'] == 'Update':
56+
comparision = broker.compare_broker_properites(event['PhysicalResourceId'], event['ResourceProperties'])
57+
if not comparision:
58+
lambda_response.respond_error("AmazonMQ resource cannot be updated. Create a new resource if changes are required.")
59+
else:
60+
response = broker.get_broker_data(event['PhysicalResourceId'], event['ResourceProperties']['MultiAZ'])
61+
lambda_response.respond(data=response)
62+
63+
elif event['RequestType'] == 'Delete':
64+
broker.delete(event['PhysicalResourceId'])
65+
lambda_response.respond()
66+
67+
except Exception as e:
68+
message = str(e)
69+
lambda_response.respond_error(message)
70+
71+
return 'OK'

amazon-mq-broker/lambda_invoker.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import boto3
2+
import os
3+
import json
4+
5+
6+
class LambdaInvoker:
7+
def __init__(self):
8+
print(f"Initialize lambda invoker")
9+
10+
def invoke(self, payload):
11+
bytes_payload = bytearray()
12+
bytes_payload.extend(map(ord, json.dumps(payload)))
13+
function_name = os.environ['AWS_LAMBDA_FUNCTION_NAME']
14+
function_payload = bytes_payload
15+
client = boto3.client('lambda')
16+
client.invoke(
17+
FunctionName=function_name,
18+
InvocationType='Event',
19+
Payload=function_payload
20+
)

amazon-mq-broker/logic.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import boto3
2+
import os
3+
import time
4+
5+
class AmazonMQBrokerLogic:
6+
7+
def __init__(self, broker_name):
8+
self.broker_name = broker_name
9+
10+
def create(self, multi_az, instance_type, user, password, security_groups, subnets):
11+
print(f"Creating AMQ instance {self.broker_name}")
12+
deployment_mode = "ACTIVE_STANDBY_MULTI_AZ" if multi_az.lower() == "true" else "SINGLE_INSTANCE"
13+
14+
client = boto3.client('mq')
15+
response = client.create_broker(
16+
AutoMinorVersionUpgrade=False,
17+
BrokerName=self.broker_name,
18+
DeploymentMode=deployment_mode,
19+
EngineType='ACTIVEMQ',
20+
EngineVersion='5.15.0',
21+
HostInstanceType=instance_type,
22+
PubliclyAccessible=False,
23+
SecurityGroups=security_groups,
24+
SubnetIds=subnets,
25+
Users=[
26+
{
27+
'ConsoleAccess': True,
28+
'Password': password,
29+
'Username': user
30+
}
31+
]
32+
)
33+
print(f"Broker Id: {response['BrokerId']} Broker Arn: {response['BrokerArn']}")
34+
active = self.endpoint(response['BrokerId'],1)
35+
response.update({'Active': active})
36+
37+
standby = self.endpoint(response['BrokerId'],2) if multi_az.lower() == "true" else "NONE"
38+
response.update({'Standby': standby})
39+
40+
print(f"Creating Amazon MQ instance\n{response}")
41+
return response
42+
43+
def wait_broker_status(self, id, lambda_context):
44+
client = boto3.client('mq')
45+
46+
while True:
47+
response = client.describe_broker(BrokerId=id)
48+
state = response['BrokerState']
49+
50+
print(f"Broker state: {state}")
51+
if state == 'RUNNING':
52+
print(f"Matched {state} - OK ")
53+
return True
54+
elif state == 'CREATION_FAILED':
55+
print(f"Matched {state} - ERROR ")
56+
return False
57+
elif lambda_context.get_remaining_time_in_millis() < 10000:
58+
print(f"Less than 10 seconds left of Lambda execution time, exiting with empty hands")
59+
return None
60+
else:
61+
print(f"Waiting for 5 seconds, time remaining" +
62+
f"in this lambda execution {lambda_context.get_remaining_time_in_millis()}ms")
63+
time.sleep(5)
64+
65+
def compare_broker_properites(self, id, properties):
66+
client = boto3.client('mq')
67+
response = client.describe_broker(BrokerId=id)
68+
69+
deployment_mode = "ACTIVE_STANDBY_MULTI_AZ" if properties['MultiAZ'].lower() == "true" else "SINGLE_INSTANCE"
70+
71+
if (properties['SecurityGroups'] == response['SecurityGroups']) and \
72+
(properties['Subnets'] == response['SubnetIds']) and \
73+
(properties['InstanceType'] == response['HostInstanceType']) and \
74+
(deployment_mode == response['DeploymentMode']) and \
75+
(properties['Name'] == response['BrokerName']):
76+
return True
77+
else:
78+
return False
79+
80+
def get_broker_data(self, id, multi_az):
81+
data = {}
82+
client = boto3.client('mq')
83+
response = client.describe_broker(BrokerId=id)
84+
85+
data.update({'BrokerId': response['BrokerId']})
86+
data.update({'BrokerArn': response['BrokerArn']})
87+
88+
active = self.endpoint(response['BrokerId'],1)
89+
data.update({'Active': active})
90+
91+
standby = self.endpoint(response['BrokerId'],2) if multi_az.lower() == "true" else "NONE"
92+
data.update({'Standby': standby})
93+
94+
return data
95+
96+
def delete(self,id):
97+
client = boto3.client('mq')
98+
client.delete_broker(
99+
BrokerId=id
100+
)
101+
102+
def endpoint(self,id,n):
103+
return f"{id}-{n}.mq.{os.environ['AWS_REGION']}.amazonaws.com"

ssm-secure-parameter/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# package marker

0 commit comments

Comments
 (0)