Files
privacy.sexy/aws/certificate-stack.yaml
undergroundwires 4e7f244190 Initial commit
2019-12-31 16:23:45 +01:00

212 lines
8.5 KiB
YAML

AWSTemplateFormatVersion: '2010-09-09'
Description: Creates certificate for the root + www subdomain. !! It must be deployed in us-east-1 to be able to be used by CloudFront.
Parameters:
RootDomainName:
Type: String
Default: privacy.sexy
Description: The root DNS name of the website e.g. privacy.sexy
AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
ConstraintDescription: Must be a valid root domain name
IamStackName:
Type: String
Default: privacysexy-iam-stack
Description: Name of the IAM stack.
Resources:
# The lambda workaround exists to be able to automate certificate deployment.
# Problem:
# Normally AWS AWS::CertificateManager::Certificate waits until a certificate is validated
# And there's no way to get validation DNS records from it to validate it.
# Solution:
# Deploy a lambda that deploys the certificate (so we don't wait until certificate is validated)
# Get DNS records to be used in validation & export it to be used later.
AcmCertificateForHostedZone:
Type: Custom::VerifiableCertificate #A Can use AWS::CloudFormation::CustomResource or Custom::String
Properties:
ServiceToken: !GetAtt ResolveCertificateLambda.Arn
# Lambda gets the following data:
RootDomainName: !Ref RootDomainName # Lambda will create both for root and www.root
Tags:
-
Key: Name
Value: !Ref RootDomainName
-
Key: Application
Value: privacy.sexy
ResolveCertificateLambda:
Type: AWS::Lambda::Function
Properties:
Description: Deploys certificate for root domain name + www and returns immediately arn + verification records.
Role:
Fn::ImportValue: !Join [':', [!Ref IamStackName, ResolveCertificateLambdaRoleArn]]
FunctionName: !Sub ${AWS::StackName}-cert-resolver-lambda # StackName- required for role to function
Handler: index.handler
Runtime: nodejs12.x
Timeout: 30
Tags:
-
Key: Application
Value: privacy.sexy
Code:
# Inline script is not the best way. Some variables are named shortly to not exceed the limit 4096 but it's the cheapest way (no s3 file)
ZipFile: >
'use strict';
const aws = require('aws-sdk');
const acm = new aws.ACM();
const log = (t) => console.log(t);
exports.handler = async (event, context) => {
log(`Request recieved:\n${JSON.stringify(event)}`);
const userData = event.ResourceProperties;
const rootDomain = userData.RootDomainName;
let data = null;
try {
switch(event.RequestType) {
case 'Create':
data = await handleCreateAsync(rootDomain, userData.Tags);
break;
case 'Update':
data = await handleUpdateAsync();
break;
case 'Delete':
data = await handleDeleteAsync(rootDomain);
break;
}
await sendResponseAsync(event, context, 'SUCCESS', data);
} catch(error) {
await sendResponseAsync(event, context, 'ERROR', {
title: `Failed to ${event.RequestType}, see error`,
error: error
});
}
}
async function handleCreateAsync(rootDomain, tags) {
const { CertificateArn } = await acm.requestCertificate({
DomainName: rootDomain,
SubjectAlternativeNames: [`www.${rootDomain}`],
Tags: tags,
ValidationMethod: 'DNS',
}).promise();
log(`Cert requested:${CertificateArn}`);
const waitAsync = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const maxAttempts = 10;
let options = undefined;
for (let attempt = 0; attempt < maxAttempts && !options; attempt++) {
await waitAsync(2000);
const { Certificate } = await acm.describeCertificate({ CertificateArn }).promise();
if(Certificate.DomainValidationOptions.filter((o) => o.ResourceRecord).length === 2) {
options = Certificate.DomainValidationOptions;
}
}
if(!options) {
throw new Error(`No records after ${maxAttempts} attempts.`);
}
return getResponseData(options, CertificateArn, rootDomain);
}
async function handleDeleteAsync(rootDomain) {
const certs = await acm.listCertificates({}).promise();
const cert = certs.CertificateSummaryList.find((cert) => cert.DomainName === rootDomain);
if (cert) {
await acm.deleteCertificate({ CertificateArn: cert.CertificateArn }).promise();
log(`Deleted ${cert.CertificateArn}`);
} else {
log('Cannot find'); // Do not fail, delete can be called when e.g. CF fails before creating cert
}
return null;
}
async function handleUpdateAsync() {
throw new Error(`Not yet implemented update`);
}
function getResponseData(options, arn, rootDomain) {
const findRecord = (url) => options.find(option => option.DomainName === url).ResourceRecord;
const root = findRecord(rootDomain);
const www = findRecord(`www.${rootDomain}`);
const data = {
CertificateArn: arn,
RootVerificationRecordName: root.Name,
RootVerificationRecordValue: root.Value,
WwwVerificationRecordName: www.Name,
WwwVerificationRecordValue: www.Value,
};
return data;
}
/* cfn-response can't async / await :( */
async function sendResponseAsync(event, context, responseStatus, responseData, physicalResourceId) {
return new Promise((s, f) => {
var b = JSON.stringify({
Status: responseStatus,
Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});
log(`Response body:\n${b}`);
var u = require("url").parse(event.ResponseURL);
var r = require("https").request(
{
hostname: u.hostname,
port: 443,
path: u.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": b.length
}
}, (p) => {
log(`Status code: ${p.statusCode}`);
log(`Status message: ${p.statusMessage}`);
s(context.done());
});
r.on("error", (e) => {
log(`request failed: ${e}`);
f(context.done(e));
});
r.write(b);
r.end();
});
}
Outputs:
CertificateArn:
Description: The Amazon Resource Name (ARN) of an AWS Certificate Manager (ACM) certificate.
Value: !GetAtt AcmCertificateForHostedZone.CertificateArn
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', CertificateArn ]]
RootVerificationRecordName:
Description: Name for root domain CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.RootVerificationRecordName
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', RootVerificationRecordName ]]
RootVerificationRecordValue:
Description: Value for root domain name CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.RootVerificationRecordValue
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', RootVerificationRecordValue ]]
WwwVerificationRecordName:
Description: Name for www domain name CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.WwwVerificationRecordName
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', WwwVerificationRecordName ]]
WwwVerificationRecordValue:
Description: Value for www domain name CNAME verification record
Value: !GetAtt AcmCertificateForHostedZone.WwwVerificationRecordValue
Export:
Name: !Join [':', [ !Ref 'AWS::StackName', WwwVerificationRecordValue ]]