Skip to content

Instantly share code, notes, and snippets.

@jprivillaso
Last active September 12, 2020 20:48
Show Gist options
  • Save jprivillaso/035bcf9b06af4c6a35216af5407a3f2e to your computer and use it in GitHub Desktop.
Save jprivillaso/035bcf9b06af4c6a35216af5407a3f2e to your computer and use it in GitHub Desktop.
AWSTemplateFormatVersion: 2010-09-09
Description: >
This template creates a lambda function that verifies an email under the current
aws account using Route53 DNS TXT records
Last Modified: 05 February 2019
Author: Juan Rivillas <jprivillaso@gmail.com>
Metadata: {}
Parameters: {}
Resources:
AmazonSesVerificationRecordsRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: 'AllowTheLambdaFunctionToAssumeThisRole'
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: Route53Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- route53:GetHostedZone
- route53:ChangeResourceRecordSets
Resource:
- Fn::Join:
- ""
- - "arn:aws:route53:::hostedzone/"
- HostedZoneID
- PolicyName: SesAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ses:VerifyDomainDkim
- ses:VerifyDomainIdentity
Resource: "*"
AmazonSesVerificationRecordsLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Description: This function manages the verification and DKIM records for SES
Code:
S3Bucket: "your-bucket-name"
S3Key: ses_route53_verification.zip
Handler: ses_route53_verification.lambda_handler
Role:
Fn::GetAtt:
- AmazonSesVerificationRecordsRole
- Arn
Runtime: python2.7
Timeout: 30
SesVerificationRecords:
Type: Custom::AmazonSesVerificationRecords
Properties:
ServiceToken:
Fn::GetAtt:
- AmazonSesVerificationRecordsLambdaFunction
- Arn
HostedZoneId:
Fn::ImportValue: !Sub ${EnvironmentName}-${Project}-HostedZonePublic1
Outputs:
# This email must be verified under SES
EmailSender:
Description: Email that will send the invoices
Value: !Ref DomainToBeVerified
Export:
Name: !Sub ${EnvironmentName}-SESEmailSender
import uuid, json, boto3
from botocore.vendored import requests
FAILED = "FAILED"
SUCCESS = "SUCCESS"
def send(event, context, responseStatus, responseData, physicalResourceId):
responseUrl = event['ResponseURL']
print responseUrl
responseBody = {}
responseBody['Status'] = responseStatus
responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + \
context.log_stream_name
responseBody['PhysicalResourceId'] = physicalResourceId
responseBody['StackId'] = event['StackId']
responseBody['RequestId'] = event['RequestId']
responseBody['LogicalResourceId'] = event['LogicalResourceId']
responseBody['Data'] = responseData
json_responseBody = json.dumps(responseBody)
print "Response body:\n" + json_responseBody
headers = {
'content-type': '',
'content-length': str(len(json_responseBody))
}
try:
response = requests.put(responseUrl,
data=json_responseBody,
headers=headers)
print "Status code: " + response.reason
except Exception as e:
print "send(..) failed executing requests.put(..): " + str(e)
def _get_hosted_zone_name(hosted_zone_id):
route53 = boto3.client('route53')
route53_resp = route53.get_hosted_zone(
Id=hosted_zone_id
)
return route53_resp['HostedZone']['Name']
def verify_ses(hosted_zone_id, action):
ses = boto3.client('ses')
print "Retrieving Hosted Zone name"
hosted_zone_name = _get_hosted_zone_name(hosted_zone_id=hosted_zone_id)
print 'Hosted zone name: {hosted_zone_name}'.format(hosted_zone_name=hosted_zone_name)
domain = hosted_zone_name.rstrip('.')
print 'domain' + domain
verification_token = ses.verify_domain_identity(
Domain=domain
)['VerificationToken']
print 'verified domain'
dkim_tokens = ses.verify_domain_dkim(
Domain=domain
)['DkimTokens']
print 'Changing resource record sets'
changes = [
{
'Action': action,
'ResourceRecordSet': {
'Name': "_amazonses.{hosted_zone_name}".format(hosted_zone_name=hosted_zone_name),
'Type': 'TXT',
'TTL': 1800,
'ResourceRecords': [
{
'Value': '"{verification_token}"'.format(verification_token=verification_token)
}
]
}
}
]
for dkim_token in dkim_tokens:
change = {
'Action': action,
'ResourceRecordSet': {
'Name': "{dkim_token}._domainkey.{hosted_zone_name}".format(
dkim_token=dkim_token,
hosted_zone_name=hosted_zone_name
),
'Type': 'CNAME',
'TTL': 1800,
'ResourceRecords': [
{
'Value': "{dkim_token}.dkim.amazonses.com".format(dkim_token=dkim_token)
}
]
}
}
changes.append(change)
boto3.client('route53').change_resource_record_sets(
ChangeBatch={
'Changes': changes
},
HostedZoneId=hosted_zone_id
)
def lambda_handler(event, context):
print "Entered the handler function: "
print "Received event: "
print event
resource_type = event['ResourceType']
request_type = event['RequestType']
resource_properties = event['ResourceProperties']
hosted_zone_id = resource_properties['HostedZoneId']
physical_resource_id = event.get(
'PhysicalResourceId', unicode(uuid.uuid4()))
print "ResourceType: " + resource_type
print "RequestType: " + request_type
print "hosted_zone_id: " + hosted_zone_id
try:
if resource_type == "Custom::AmazonSesVerificationRecords":
if request_type == 'Create':
verify_ses(hosted_zone_id=hosted_zone_id, action='UPSERT')
elif request_type == 'Delete':
verify_ses(hosted_zone_id=hosted_zone_id, action='DELETE')
elif request_type == 'Update':
old_hosted_zone_id = event['OldResourceProperties']['HostedZoneId']
verify_ses(hosted_zone_id=old_hosted_zone_id, action='DELETE')
verify_ses(hosted_zone_id=hosted_zone_id, action='UPSERT')
else:
print 'Request type is {request_type}, doing nothing.'.format(request_type=request_type)
response_data = {}
else:
raise ValueError("Unexpected resource_type: {resource_type}".format(
resource_type=resource_type))
except Exception:
send(
event,
context,
responseStatus=FAILED if request_type != 'Delete' else SUCCESS,
# Do not fail on delete to avoid rollback failure
responseData=None,
physicalResourceId=physical_resource_id
)
# this statement is important so the exception (along with the original traceback) is logged to Cloudwatch
raise
else:
send(
event,
context,
responseStatus=SUCCESS,
responseData=response_data,
physicalResourceId=physical_resource_id,
)
@fchorney
Copy link

You the real MVP! Found the medium article about this, and was also having trouble since a lot of things weren't defined properly. Thanks for posting this :)

@theneverstill
Copy link

This is very helpful, thanks! Note: The medium article and this code neglect to call ses.delete_identity on request_type = Delete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment