Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(acm-certificatemanager): DnsValidatedCertificateHandler support for SubjectAlternativeNames #7050

Merged
merged 6 commits into from
Apr 1, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,24 @@ const requestCertificate = async function(requestId, domainName, subjectAlternat

console.log('Waiting for ACM to provide DNS records for validation...');

let record;
for (let attempt = 0; attempt < maxAttempts && !record; attempt++) {
let records;
for (let attempt = 0; attempt < maxAttempts && !records; attempt++) {
const { Certificate } = await acm.describeCertificate({
CertificateArn: reqCertResponse.CertificateArn
}).promise();
const options = Certificate.DomainValidationOptions || [];

if (options.length > 0 && options[0].ResourceRecord) {
record = options[0].ResourceRecord;
// some alternative names will produce the same validation record
// as the main domain (eg. example.com + *.example.com)
// filtering duplicates to avoid errors with adding the same record
// to the route53 zone twice
const unique = options
.map((val) => val.ResourceRecord)
.reduce((acc, cur) => {
acc[cur.Name] = cur;
return acc;
}, {});
records = Object.keys(unique).sort().map(key => unique[key]);
} else {
// Exponential backoff with jitter based on 200ms base
// component of backoff fixed to ensure minimum total wait time on
Expand All @@ -116,25 +125,28 @@ const requestCertificate = async function(requestId, domainName, subjectAlternat
await sleep(random() * base * 50 + base * 150);
}
}
if (!record) {
if (!records) {
throw new Error(`Response from describeCertificate did not contain DomainValidationOptions after ${maxAttempts} attempts.`)
}

console.log(`Upserting DNS record into zone ${hostedZoneId}: ${record.Name} ${record.Type} ${record.Value}`);
console.log(`Upserting ${records.length} DNS records into zone ${hostedZoneId}:`);

const changeBatch = await route53.changeResourceRecordSets({
ChangeBatch: {
Changes: [{
Action: 'UPSERT',
ResourceRecordSet: {
Name: record.Name,
Type: record.Type,
TTL: 60,
ResourceRecords: [{
Value: record.Value
}]
}
}]
Changes: records.map((record) => {
console.log(`${record.Name} ${record.Type} ${record.Value}`)
return {
Action: 'UPSERT',
ResourceRecordSet: {
Name: record.Name,
Type: record.Type,
TTL: 60,
ResourceRecords: [{
Value: record.Value
}]
}
};
}),
},
HostedZoneId: hostedZoneId
}).promise();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ describe('DNS Validated Certificate Handler', () => {
let origLog = console.log;
const testRequestId = 'f4ef1b10-c39a-44e3-99c0-fbf7e53c3943';
const testDomainName = 'test.example.com';
const testSubjectAlternativeName = 'foo.example.com';
const testHostedZoneId = '/hostedzone/Z3P5QSUBK4POTI';
const testCertificateArn = 'arn:aws:acm:region:123456789012:certificate/12345678-1234-1234-1234-123456789012';
const testRRName = '_3639ac514e785e898d2646601fa951d5.example.com';
const testRRValue = '_x2.acm-validations.aws';
const testAltRRName = '_3639ac514e785e898d2646601fa951d5.foo.example.com';
const testAltRRValue = '_x3.acm-validations.aws';
const spySleep = sinon.spy(function(ms) {
return Promise.resolve();
});
Expand Down Expand Up @@ -145,6 +148,205 @@ describe('DNS Validated Certificate Handler', () => {
});
});

test('Create operation with `SubjectAlternativeNames` requests a certificate with validation records for all options', () => {
const requestCertificateFake = sinon.fake.resolves({
CertificateArn: testCertificateArn,
});

const describeCertificateFake = sinon.stub();
describeCertificateFake.onFirstCall().resolves({
Certificate: {
CertificateArn: testCertificateArn
}
});
describeCertificateFake.resolves({
Certificate: {
CertificateArn: testCertificateArn,
DomainValidationOptions: [
{
ValidationStatus: 'SUCCESS',
ResourceRecord: {
Name: testRRName,
Type: 'CNAME',
Value: testRRValue
}
}, {
ValidationStatus: 'SUCCESS',
ResourceRecord: {
Name: testAltRRName,
Type: 'CNAME',
Value: testAltRRValue
}
}
]
}
});

const changeResourceRecordSetsFake = sinon.fake.resolves({
ChangeInfo: {
Id: 'bogus'
}
});

AWS.mock('ACM', 'requestCertificate', requestCertificateFake);
AWS.mock('ACM', 'describeCertificate', describeCertificateFake);
AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsFake);

const request = nock(ResponseURL).put('/', body => {
return body.Status === 'SUCCESS';
}).reply(200);

return LambdaTester(handler.certificateRequestHandler)
.event({
RequestType: 'Create',
RequestId: testRequestId,
ResourceProperties: {
DomainName: testDomainName,
SubjectAlternativeNames: [testSubjectAlternativeName],
HostedZoneId: testHostedZoneId,
Region: 'us-east-1',
}
})
.expectResolve(() => {
sinon.assert.calledWith(requestCertificateFake, sinon.match({
DomainName: testDomainName,
ValidationMethod: 'DNS',
SubjectAlternativeNames: [testSubjectAlternativeName]
}));
sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({
ChangeBatch: {
Changes: [
{
Action: 'UPSERT',
ResourceRecordSet: {
Name: testRRName,
Type: 'CNAME',
TTL: 60,
ResourceRecords: [{
Value: testRRValue
}]
}
}, {
Action: 'UPSERT',
ResourceRecordSet: {
Name: testAltRRName,
Type: 'CNAME',
TTL: 60,
ResourceRecords: [{
Value: testAltRRValue
}]
}
}
]
},
HostedZoneId: testHostedZoneId
}));
expect(request.isDone()).toBe(true);
});
});

test('Create operation with `SubjectAlternativeNames` requests a certificate for all options without duplicates', () => {
const requestCertificateFake = sinon.fake.resolves({
CertificateArn: testCertificateArn,
});

const describeCertificateFake = sinon.stub();
describeCertificateFake.onFirstCall().resolves({
Certificate: {
CertificateArn: testCertificateArn
}
});
describeCertificateFake.resolves({
Certificate: {
CertificateArn: testCertificateArn,
DomainValidationOptions: [
{
ValidationStatus: 'SUCCESS',
ResourceRecord: {
Name: testRRName,
Type: 'CNAME',
Value: testRRValue
}
}, {
ValidationStatus: 'SUCCESS',
ResourceRecord: {
Name: testAltRRName,
Type: 'CNAME',
Value: testAltRRValue
}
}, {
ValidationStatus: 'SUCCESS',
ResourceRecord: {
Name: testRRName,
Type: 'CNAME',
Value: testRRValue
}
}
]
}
});

const changeResourceRecordSetsFake = sinon.fake.resolves({
ChangeInfo: {
Id: 'bogus'
}
});

AWS.mock('ACM', 'requestCertificate', requestCertificateFake);
AWS.mock('ACM', 'describeCertificate', describeCertificateFake);
AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsFake);

const request = nock(ResponseURL).put('/', body => {
return body.Status === 'SUCCESS';
}).reply(200);

return LambdaTester(handler.certificateRequestHandler)
.event({
RequestType: 'Create',
RequestId: testRequestId,
ResourceProperties: {
DomainName: testDomainName,
HostedZoneId: testHostedZoneId,
Region: 'us-east-1',
}
})
.expectResolve(() => {
sinon.assert.calledWith(requestCertificateFake, sinon.match({
DomainName: testDomainName,
ValidationMethod: 'DNS'
}));
sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({
ChangeBatch: {
Changes: [
{
Action: 'UPSERT',
ResourceRecordSet: {
Name: testRRName,
Type: 'CNAME',
TTL: 60,
ResourceRecords: [{
Value: testRRValue
}]
}
}, {
Action: 'UPSERT',
ResourceRecordSet: {
Name: testAltRRName,
Type: 'CNAME',
TTL: 60,
ResourceRecords: [{
Value: testAltRRValue
}]
}
}
]
},
HostedZoneId: testHostedZoneId
}));
expect(request.isDone()).toBe(true);
});
});

test('Create operation fails after more than 60s if certificate has no DomainValidationOptions', () => {
handler.withRandom(() => 0);
const requestCertificateFake = sinon.fake.resolves({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3Bucket3747EA0C"
"Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3BucketFCCD3A76"
},
"S3Key": {
"Fn::Join": [
Expand All @@ -558,7 +558,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F"
"Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6"
}
]
}
Expand All @@ -571,7 +571,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F"
"Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6"
}
]
}
Expand Down Expand Up @@ -865,17 +865,17 @@
}
},
"Parameters": {
"AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3Bucket3747EA0C": {
"AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3BucketFCCD3A76": {
"Type": "String",
"Description": "S3 bucket for asset \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\""
"Description": "S3 bucket for asset \"19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2\""
},
"AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F": {
"AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6": {
"Type": "String",
"Description": "S3 key for asset version \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\""
"Description": "S3 key for asset version \"19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2\""
},
"AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247ArtifactHashFB4438F1": {
"AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2ArtifactHash652C125C": {
"Type": "String",
"Description": "Artifact hash for asset \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\""
"Description": "Artifact hash for asset \"19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2\""
}
}
}