From 95f4aac14f3ba70f8b824c315087d52d05d0ee34 Mon Sep 17 00:00:00 2001 From: nic Date: Fri, 27 Mar 2020 23:24:01 +0100 Subject: [PATCH 1/4] fix(acm-certificatemanager): add support for DNS validation of certificates with `SubjectAlternativeNames` fixes #4659 --- .../lib/index.js | 46 ++-- .../test/handler.test.js | 202 ++++++++++++++++++ 2 files changed, 231 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js index ab112a3b248f0..3f853a6d125c9 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js @@ -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 @@ -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(); diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js index 5b3378e93b524..3e93f09680a91 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/test/handler.test.js @@ -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(); }); @@ -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({ From 4120a34fdd9f252a8cd5274ab4fb504382028625 Mon Sep 17 00:00:00 2001 From: nic Date: Tue, 31 Mar 2020 22:56:28 +0200 Subject: [PATCH 2/4] fix(acm-certificatemanager): DnsValidatedCertificate doesn't wait long enough fixes #5831 changed parameter references --- .../dns_validated_certificate_handler/lib/index.js | 2 +- .../integ.load-balanced-fargate-service.expected.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js index 3f853a6d125c9..866e9405b049e 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js @@ -116,7 +116,7 @@ const requestCertificate = async function(requestId, domainName, subjectAlternat acc[cur.Name] = cur; return acc; }, {}); - records = Object.keys(unique).sort().map((key) => unique[key]); + 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 diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index ba5017dd9e617..40e66a24cb936 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -545,7 +545,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3Bucket3747EA0C" + "Ref": "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3Bucket7A5FA028" }, "S3Key": { "Fn::Join": [ @@ -558,7 +558,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F" + "Ref": "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3VersionKeyD02759A5" } ] } @@ -571,7 +571,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F" + "Ref": "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3VersionKeyD02759A5" } ] } @@ -865,11 +865,11 @@ } }, "Parameters": { - "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3Bucket3747EA0C": { + "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3Bucket7A5FA028": { "Type": "String", "Description": "S3 bucket for asset \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" }, - "AssetParameters01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247S3VersionKey13E25E1F": { + "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3VersionKeyD02759A5": { "Type": "String", "Description": "S3 key for asset version \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" }, From 2f92e3eca5abfa0c744e51b71a04455b40204844 Mon Sep 17 00:00:00 2001 From: nic Date: Tue, 31 Mar 2020 23:23:04 +0200 Subject: [PATCH 3/4] fix(acm-certificatemanager): DnsValidatedCertificate doesn't wait long enough fixes #5831 changed parameter references --- .../integ.load-balanced-fargate-service.expected.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index 40e66a24cb936..5317695311f33 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -545,7 +545,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3Bucket7A5FA028" + "Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3BucketFCCD3A76" }, "S3Key": { "Fn::Join": [ @@ -558,7 +558,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3VersionKeyD02759A5" + "Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6" } ] } @@ -571,7 +571,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3VersionKeyD02759A5" + "Ref": "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6" } ] } @@ -865,11 +865,11 @@ } }, "Parameters": { - "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3Bucket7A5FA028": { + "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3BucketFCCD3A76": { "Type": "String", "Description": "S3 bucket for asset \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" }, - "AssetParameters32949dc2510e9dcfb770033a67b684a0ed49f86fb8a6ed93e2ccafbb4217641dS3VersionKeyD02759A5": { + "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3VersionKey07AF06B6": { "Type": "String", "Description": "S3 key for asset version \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" }, From 8ff9932196f0e78bd90e450e17947d4c309437f8 Mon Sep 17 00:00:00 2001 From: nic Date: Wed, 1 Apr 2020 00:15:00 +0200 Subject: [PATCH 4/4] fix(acm-certificatemanager): DnsValidatedCertificate doesn't wait long enough fixes #5831 changed parameter references --- .../integ.load-balanced-fargate-service.expected.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index 5317695311f33..d01d8b9bfd6d9 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -867,15 +867,15 @@ "Parameters": { "AssetParameters19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2S3BucketFCCD3A76": { "Type": "String", - "Description": "S3 bucket for asset \"01b2187f99280c53b7d58040d494b5d051e1e253601fc32dee62ba56712db247\"" + "Description": "S3 bucket for asset \"19e461d2ff1a5b90438fed6ceee4c197d7efee8712a6f76d85b501ab20bfb1a2\"" }, "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\"" } } }