Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,105 @@ Required parameters:
- `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
- `Transform` - Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass AWS WAF.
Implementation require to be serialised with other waf condition.
### AmazonMQ Broker

This custom resource creates a AmazonMQ broker instance.

**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.

handler: `amazon-mq-broker/handler.lambda_handler`
runtime: `python3.6`

Required parameters:

- `Name` - Unique name given to the broker
- `SecurityGroups` - Array of security group ids
- `Subnets` - Array of subnets ids
- `MultiAZ` - String boolean [ 'true', 'false' ]
- `InstanceType` - valid values [ 'mq.t2.micro', 'mq.m4.large' ]
- `Username` - Username for the amq user
- `Password` - Password for the amq user. Must be 12-250 characters long

No optional parameters.

Returned Values:

- `Active` - Active AmazonMQ endpoint
- `Stanby` - Standby AmazonMQ endpoint
- `BrokerId` - Id of the AmazonMQ Broker
- `BrokerArn` - Arn of the broker

IAM Permissions:

```json
{
"Statement":
[
{
"Effect": "Allow",
"Action":
[
"mq:*",
"ec2:CreateNetworkInterface",
"ec2:CreateNetworkInterfacePermission",
"ec2:DeleteNetworkInterface",
"ec2:DeleteNetworkInterfacePermission",
"ec2:DetachNetworkInterface",
"ec2:DescribeInternetGateways",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeNetworkInterfacePermissions",
"ec2:DescribeRouteTables",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeVpcs",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"lambda:InvokeFunction"
],
"Resource": ["*"]
}
]
}
```

### Auto generated secure ssm parameters

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.

handler: `ssm-secure-parameter/handler.lambda_handler`
runtime: `python3.6`

Required parameters:

- `Path` - SSM parameter path e.g. `/app/env/password`

Optional parameters:

- `Length` - Length of the auto generated password. Defaults to 16 characters

Returned Values:

- `Password` - The password generated by the resource

IAM Permissions:

```json
{
"Statement":
[
{
"Effect": "Allow",
"Action":
[
"ssm:PutParameter",
"ssm:DeleteParameter",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": ["*"]
}
]
}
```
1 change: 1 addition & 0 deletions amazon-mq-broker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# package marker
58 changes: 58 additions & 0 deletions amazon-mq-broker/cr_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging
from urllib.request import urlopen, Request, HTTPError, URLError
import json

logger = logging.getLogger()
logger.setLevel(logging.INFO)


class CustomResourceResponse:
def __init__(self, request_payload):
self.payload = request_payload
self.response = {
"StackId": request_payload["StackId"],
"RequestId": request_payload["RequestId"],
"LogicalResourceId": request_payload["LogicalResourceId"],
"Status": 'SUCCESS',
}

def respond_error(self, message):
self.response['Status'] = 'FAILED'
self.response['Reason'] = message
self.respond()

def respond(self, data=None):
event = self.payload
response = self.response
####
#### copied from https://github.com/ryansb/cfn-wrapper-python/blob/master/cfn_resource.py
####

if event.get("PhysicalResourceId", False):
response["PhysicalResourceId"] = event["PhysicalResourceId"]

if data is not None:
response['Data'] = data

logger.debug("Received %s request with event: %s" % (event['RequestType'], json.dumps(event)))

serialized = json.dumps(response)
logger.info(f"Responding to {event['RequestType']} request with: {serialized}")

req_data = serialized.encode('utf-8')

req = Request(
event['ResponseURL'],
data=req_data,
headers={'Content-Length': len(req_data),'Content-Type': ''}
)
req.get_method = lambda: 'PUT'

try:
urlopen(req)
logger.debug("Request to CFN API succeeded, nothing to do here")
except HTTPError as e:
logger.error("Callback to CFN API failed with status %d" % e.code)
logger.error("Response: %s" % e.reason)
except URLError as e:
logger.error("Failed to reach the server - %s" % e.reason)
71 changes: 71 additions & 0 deletions amazon-mq-broker/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import sys
import os
import json

sys.path.append(f"{os.environ['LAMBDA_TASK_ROOT']}/lib")
sys.path.append(os.path.dirname(os.path.realpath(__file__)))

import cr_response
import logic
import lambda_invoker

def lambda_handler(event, context):

print(f"Received event:{json.dumps(event)}")

lambda_response = cr_response.CustomResourceResponse(event)
cr_params = event['ResourceProperties']

# Validate input
for key in ['MultiAZ', 'InstanceType', 'Username', 'Password', 'SecurityGroups', 'Subnets']:
if key not in cr_params:
lambda_response.respond_error(f"{key} property missing")
return

try:
broker = logic.AmazonMQBrokerLogic(cr_params['Name'])
if event['RequestType'] == 'Create':
if ('WaitComplete' in event) and (event['WaitComplete']):
result = broker.wait_broker_status(event['PhysicalResourceId'], context)

if result is None:
invoke = lambda_invoker.LambdaInvoker()
invoke.invoke(event)
elif result:
lambda_response.respond(data=event['Data'])
elif not result:
lambda_response.respond_error(f"Creation of AmazonMQ {event['PhysicalResourceId']} failed.")

else:
response = broker.create(
multi_az=cr_params['MultiAZ'],
instance_type=cr_params['InstanceType'],
user=cr_params['Username'],
password=cr_params['Password'],
security_groups=cr_params['SecurityGroups'],
subnets=cr_params['Subnets']
)

event['PhysicalResourceId'] = response['BrokerId']
event['Data'] = response
event['WaitComplete'] = True
invoke = lambda_invoker.LambdaInvoker()
invoke.invoke(event)

elif event['RequestType'] == 'Update':
comparision = broker.compare_broker_properites(event['PhysicalResourceId'], event['ResourceProperties'])
if not comparision:
lambda_response.respond_error("AmazonMQ resource cannot be updated. Create a new resource if changes are required.")
else:
response = broker.get_broker_data(event['PhysicalResourceId'], event['ResourceProperties']['MultiAZ'])
lambda_response.respond(data=response)

elif event['RequestType'] == 'Delete':
broker.delete(event['PhysicalResourceId'])
lambda_response.respond()

except Exception as e:
message = str(e)
lambda_response.respond_error(message)

return 'OK'
20 changes: 20 additions & 0 deletions amazon-mq-broker/lambda_invoker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import boto3
import os
import json


class LambdaInvoker:
def __init__(self):
print(f"Initialize lambda invoker")

def invoke(self, payload):
bytes_payload = bytearray()
bytes_payload.extend(map(ord, json.dumps(payload)))
function_name = os.environ['AWS_LAMBDA_FUNCTION_NAME']
function_payload = bytes_payload
client = boto3.client('lambda')
client.invoke(
FunctionName=function_name,
InvocationType='Event',
Payload=function_payload
)
103 changes: 103 additions & 0 deletions amazon-mq-broker/logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import boto3
import os
import time

class AmazonMQBrokerLogic:

def __init__(self, broker_name):
self.broker_name = broker_name

def create(self, multi_az, instance_type, user, password, security_groups, subnets):
print(f"Creating AMQ instance {self.broker_name}")
deployment_mode = "ACTIVE_STANDBY_MULTI_AZ" if multi_az.lower() == "true" else "SINGLE_INSTANCE"

client = boto3.client('mq')
response = client.create_broker(
AutoMinorVersionUpgrade=False,
BrokerName=self.broker_name,
DeploymentMode=deployment_mode,
EngineType='ACTIVEMQ',
EngineVersion='5.15.0',
HostInstanceType=instance_type,
PubliclyAccessible=False,
SecurityGroups=security_groups,
SubnetIds=subnets,
Users=[
{
'ConsoleAccess': True,
'Password': password,
'Username': user
}
]
)
print(f"Broker Id: {response['BrokerId']} Broker Arn: {response['BrokerArn']}")
active = self.endpoint(response['BrokerId'],1)
response.update({'Active': active})

standby = self.endpoint(response['BrokerId'],2) if multi_az.lower() == "true" else "NONE"
response.update({'Standby': standby})

print(f"Creating Amazon MQ instance\n{response}")
return response

def wait_broker_status(self, id, lambda_context):
client = boto3.client('mq')

while True:
response = client.describe_broker(BrokerId=id)
state = response['BrokerState']

print(f"Broker state: {state}")
if state == 'RUNNING':
print(f"Matched {state} - OK ")
return True
elif state == 'CREATION_FAILED':
print(f"Matched {state} - ERROR ")
return False
elif lambda_context.get_remaining_time_in_millis() < 10000:
print(f"Less than 10 seconds left of Lambda execution time, exiting with empty hands")
return None
else:
print(f"Waiting for 5 seconds, time remaining" +
f"in this lambda execution {lambda_context.get_remaining_time_in_millis()}ms")
time.sleep(5)

def compare_broker_properites(self, id, properties):
client = boto3.client('mq')
response = client.describe_broker(BrokerId=id)

deployment_mode = "ACTIVE_STANDBY_MULTI_AZ" if properties['MultiAZ'].lower() == "true" else "SINGLE_INSTANCE"

if (properties['SecurityGroups'] == response['SecurityGroups']) and \
(properties['Subnets'] == response['SubnetIds']) and \
(properties['InstanceType'] == response['HostInstanceType']) and \
(deployment_mode == response['DeploymentMode']) and \
(properties['Name'] == response['BrokerName']):
return True
else:
return False

def get_broker_data(self, id, multi_az):
data = {}
client = boto3.client('mq')
response = client.describe_broker(BrokerId=id)

data.update({'BrokerId': response['BrokerId']})
data.update({'BrokerArn': response['BrokerArn']})

active = self.endpoint(response['BrokerId'],1)
data.update({'Active': active})

standby = self.endpoint(response['BrokerId'],2) if multi_az.lower() == "true" else "NONE"
data.update({'Standby': standby})

return data

def delete(self,id):
client = boto3.client('mq')
client.delete_broker(
BrokerId=id
)

def endpoint(self,id,n):
return f"{id}-{n}.mq.{os.environ['AWS_REGION']}.amazonaws.com"
1 change: 1 addition & 0 deletions ssm-secure-parameter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# package marker
Loading