Skip to content

Commit

Permalink
feat(certificate-manager): native CloudFormation DNS validated certif…
Browse files Browse the repository at this point in the history
…icate

Automatically adding Amazon Route 53 CNAME records for DNS validation is
now natively supported by CloudFormation.

Add a `validation` prop to `Certificate` to handle both email and DNS
validation. Deprecate `DnsValidatedCertificate`.

The default remains email validation (non-breaking).

Closes aws#5831
Closes aws#5835
Closes aws#6081
Closes aws#6516
Closes aws#7150
Closes aws#7941
Closes aws#7995
Closes aws#7996
  • Loading branch information
jogold committed Jun 15, 2020
1 parent 50f4a21 commit 89122f9
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 75 deletions.
29 changes: 18 additions & 11 deletions packages/@aws-cdk/aws-certificatemanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,26 @@ records for your domain.
See [Validate with DNS](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html)
in the Amazon Certificate Manager User Guide.

### Automatic DNS-validated certificates using Route53
```ts
new Certificate(this, 'Certificate', {
domainName: 'hello.example.com',
validation: CertificateValidation.fromDns(),
});
```

If Amazon Route 53 is your DNS provider for the requested domain, the DNS record can be
created automatically:

The `DnsValidatedCertificateRequest` class provides a Custom Resource by which
you can request a TLS certificate from AWS Certificate Manager that is
automatically validated using a cryptographically secure DNS record. For this to
work, there must be a Route 53 public zone that is responsible for serving
records under the Domain Name of the requested certificate. For example, if you
request a certificate for `www.example.com`, there must be a Route 53 public
zone `example.com` that provides authoritative records for the domain.
```ts
new Certificate(this, 'Certificate', {
domainName: 'hello.example.com',
validation: CertificateValidation.fromDns(myHostedZone),
});
```

Example:
When working with multiple domains, you can specify a default validation hosted zone:

[request a validated certificate example](test/example.dns-validated-request.lit.ts)
[multiple domains DNS validation](test/example.dns.lit.ts)

### Importing

Expand All @@ -71,4 +78,4 @@ const certificate = Certificate.fromCertificateArn(this, 'Certificate', arn);
### Sharing between Stacks

To share the certificate between stacks in the same CDK application, simply
pass the `Certificate` object between the stacks.
pass the `Certificate` object between the stacks.
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ const deleteCertificate = async function(arn, region) {
* Main handler, invoked by Lambda
*/
exports.certificateRequestHandler = async function(event, context) {
console.log('The `DnsValidatedCertificate` construct is deprecated. See `validation` prop in `Certificate`.')
var responseData = {};
var physicalResourceId;
var certificateArn;
Expand Down
183 changes: 148 additions & 35 deletions packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as route53 from '@aws-cdk/aws-route53';
import { Construct, IResource, Resource, Token } from '@aws-cdk/core';
import { CfnCertificate } from './certificatemanager.generated';
import { CfnCertificate } from './certificatemanager.generated';
import { apexDomain } from './util';

/**
Expand Down Expand Up @@ -40,33 +41,120 @@ export interface CertificateProps {
* Has to be a superdomain of the requested domain.
*
* @default - Apex domain is used for every domain that's not overridden.
* @deprecated use `validation` instead.
*/
readonly validationDomains?: {[domainName: string]: string};

/**
* Validation method used to assert domain ownership
*
* @default ValidationMethod.EMAIL
* @deprecated use `validation` instead.
*/
readonly validationMethod?: ValidationMethod;

/**
* How to validate this certifcate
*
* @default CertificateValidation.fromEmail()
*/
readonly validation?: CertificateValidation;
}

/**
* Properties for certificate validation
*/
export interface CertificationValidationProps {
/**
* Validation method
*
* @default ValidationMethod.EMAIL
*/
readonly method?: ValidationMethod;

/**
* Hosted zone to use for DNS validation
*
* @default - use email validation
*/
readonly hostedZone?: route53.IHostedZone;

/**
* A map of hosted zone to use for DNS validation
*
* @default - use `hostedZone`
*/
readonly hostedZones?: { [domainName: string]: route53.IHostedZone };

/**
* Validation domains to use for email validation
*
* @default - Apex domain
*/
readonly validationDomains?: { [domainName: string]: string };
}

/**
* How to validate a certificate
*/
export class CertificateValidation {
/**
* Validate the certifcate with DNS
*
* IMPORTANT: If neither `hostedZone` nor `hostedZones` is specified, DNS records
* must be added manually and the stack will not complete creating until you the
* records are added.
*
* @param hostedZone the default hosted zone to use for all domains in the certificate
* @param hostedZones a map of hosted zones to use for domains in the certificate
*/
public static fromDns(hostedZone?: route53.IHostedZone, hostedZones?: { [domainName: string]: route53.IHostedZone }) {
return new CertificateValidation({
method: ValidationMethod.DNS,
hostedZone,
hostedZones,
});
}

/**
* Validate the certifcate with Email
*
* IMPORTANT: if you are creating a certificate as part of your stack, the stack
* will not complete creating until you read and follow the instructions in the
* email that you will receive.
*
* ACM will send validation emails to the following addresses:
*
* admin@domain.com
* administrator@domain.com
* hostmaster@domain.com
* postmaster@domain.com
* webmaster@domain.com
*
* For every domain that you register.
*
* @param validationDomains a map of validation domains to use for domains in the certificate
*/
public static fromEmail(validationDomains?: { [domainName: string]: string }) {
return new CertificateValidation({
method: ValidationMethod.EMAIL,
validationDomains,
});
}

/**
* The validation method
*/
public readonly method: ValidationMethod;

/** @param props Certification validation properties */
private constructor(public readonly props: CertificationValidationProps) {
this.method = props.method ?? ValidationMethod.EMAIL;
}
}

/**
* A certificate managed by AWS Certificate Manager
*
* IMPORTANT: if you are creating a certificate as part of your stack, the stack
* will not complete creating until you read and follow the instructions in the
* email that you will receive.
*
* ACM will send validation emails to the following addresses:
*
* admin@domain.com
* administrator@domain.com
* hostmaster@domain.com
* postmaster@domain.com
* webmaster@domain.com
*
* For every domain that you register.
*/
export class Certificate extends Resource implements ICertificate {

Expand All @@ -89,33 +177,29 @@ export class Certificate extends Resource implements ICertificate {
constructor(scope: Construct, id: string, props: CertificateProps) {
super(scope, id);

let validation: CertificateValidation;
if (props.validation) {
validation = props.validation;
} else { // Deprecated props
if (props.validationMethod === ValidationMethod.DNS) {
validation = CertificateValidation.fromDns();
} else if (props.validationDomains) {
validation = CertificateValidation.fromEmail(props.validationDomains);
} else {
validation = CertificateValidation.fromEmail();
}
}

const allDomainNames = [props.domainName].concat(props.subjectAlternativeNames || []);

const cert = new CfnCertificate(this, 'Resource', {
domainName: props.domainName,
subjectAlternativeNames: props.subjectAlternativeNames,
domainValidationOptions: allDomainNames.map(domainValidationOption),
validationMethod: props.validationMethod,
domainValidationOptions: renderDomainValidation(validation, allDomainNames),
validationMethod: validation.method,
});

this.certificateArn = cert.ref;

/**
* Return the domain validation options for the given domain
*
* Closes over props.
*/
function domainValidationOption(domainName: string): CfnCertificate.DomainValidationOptionProperty {
let validationDomain = props.validationDomains && props.validationDomains[domainName];
if (validationDomain === undefined) {
if (Token.isUnresolved(domainName)) {
throw new Error('When using Tokens for domain names, \'validationDomains\' needs to be supplied');
}
validationDomain = apexDomain(domainName);
}

return { domainName, validationDomain };
}
}
}

Expand All @@ -136,4 +220,33 @@ export enum ValidationMethod {
* @see https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html
*/
DNS = 'DNS',
}
}

// tslint:disable-next-line:max-line-length
function renderDomainValidation(validation: CertificateValidation, domainNames: string[]): CfnCertificate.DomainValidationOptionProperty[] | undefined {
const domainValidation: CfnCertificate.DomainValidationOptionProperty[] = [];

switch (validation.method) {
case ValidationMethod.DNS:
for (const domainName of domainNames) {
const hostedZone = validation.props.hostedZones?.[domainName] ?? validation.props.hostedZone;
if (hostedZone) {
domainValidation.push({ domainName, hostedZoneId: hostedZone.hostedZoneId });
}
}
break;
case ValidationMethod.EMAIL:
for (const domainName of domainNames) {
const validationDomain = validation.props.validationDomains?.[domainName];
if (!validationDomain && Token.isUnresolved(domainName)) {
throw new Error('When using Tokens for domain names, \'validationDomains\' needs to be supplied');
}
domainValidation.push({ domainName, validationDomain: validationDomain ?? apexDomain(domainName) });
}
break;
default:
throw new Error(`Unknown validation method ${validation.method}`);
}

return domainValidation.length !== 0 ? domainValidation : undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { CertificateProps, ICertificate } from './certificate';
/**
* Properties to create a DNS validated certificate managed by AWS Certificate Manager
*
* @experimental
* @deprecated use the `validation` prop with `CertificateValidation.fromDns()`
* on `Certificate`.
*/
export interface DnsValidatedCertificateProps extends CertificateProps {
/**
Expand Down Expand Up @@ -52,7 +53,8 @@ export interface DnsValidatedCertificateProps extends CertificateProps {
* validated using DNS validation against the specified Route 53 hosted zone.
*
* @resource AWS::CertificateManager::Certificate
* @experimental
* @deprecated use the `validation` prop with `CertificateValidation.fromDns()`
* on `Certificate`.
*/
export class DnsValidatedCertificate extends cdk.Resource implements ICertificate {
public readonly certificateArn: string;
Expand Down

This file was deleted.

34 changes: 34 additions & 0 deletions packages/@aws-cdk/aws-certificatemanager/test/example.dns.lit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as route53 from '@aws-cdk/aws-route53';
import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core';
import * as acm from '../lib';

class AcmStack extends Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
/// !show
const exampleCom = new route53.HostedZone(this, 'ExampleCom', {
zoneName: 'example.com',
});

const exampleNet = new route53.HostedZone(this, 'ExampelNet', {
zoneName: 'example.net',
});

const cert = new acm.Certificate(this, 'Certificate', {
domainName: 'test.example.com',
subjectAlternativeNames: ['cool.example.com', 'test.example.net'],
validation: acm.CertificateValidation.fromDns(exampleCom, {
'test.example.net': exampleNet,
}),
});
/// !hide

new CfnOutput(this, 'Output', {
value: cert.certificateArn,
});
}
}

const app = new App();
new AcmStack(app, 'AcmStack');
app.synth();
Loading

0 comments on commit 89122f9

Please sign in to comment.