diff --git a/integ/lib/render-struct.ts b/integ/lib/render-struct.ts index 667514e61..4a9237013 100644 --- a/integ/lib/render-struct.ts +++ b/integ/lib/render-struct.ts @@ -11,7 +11,9 @@ import { X509CertificatePem } from 'aws-rfdk'; import { IRepository, RenderQueue, + RenderQueueHostNameProps, RenderQueueProps, + RenderQueueTrafficEncryptionProps, ThinkboxDockerRecipes, } from 'aws-rfdk/deadline'; import { ThinkboxDockerImageOverrides } from './ThinkboxDockerImageOverrides'; @@ -52,9 +54,9 @@ export class RenderStruct extends Construct { const maxLength = 64 - host.length - '.'.length - suffix.length - 1; const zoneName = Stack.of(this).stackName.slice(0, maxLength) + suffix; - let trafficEncryption: any; - let hostname: any; - let cacert: any; + let trafficEncryption: RenderQueueTrafficEncryptionProps | undefined; + let hostname: RenderQueueHostNameProps | undefined; + let cacert: X509CertificatePem | undefined; // If configured for HTTPS, the render queue requires a private domain and a signed certificate for authentication if( props.protocol === 'https' ) { @@ -72,8 +74,8 @@ export class RenderStruct extends Construct { }, signingCertificate: cacert, }), - internalProtocol: ApplicationProtocol.HTTP, }, + internalProtocol: ApplicationProtocol.HTTP, }; hostname = { zone: new PrivateHostedZone(this, 'Zone', { @@ -83,7 +85,7 @@ export class RenderStruct extends Construct { hostname: host, }; } else { - trafficEncryption = undefined; + trafficEncryption = { externalTLS: { enabled: false } }; hostname = undefined; } diff --git a/packages/aws-rfdk/docs/upgrade/index.md b/packages/aws-rfdk/docs/upgrade/index.md index 254a7f779..7d2d1290c 100644 --- a/packages/aws-rfdk/docs/upgrade/index.md +++ b/packages/aws-rfdk/docs/upgrade/index.md @@ -5,3 +5,4 @@ applications. The documentation is separated by RFDK versions that included pote upgrading to (or beyond) a version listed below, you should consult the the linked upgrade documentation. * [`0.27.x`](./upgrading-0.27.md) +* [`0.37.x`](./upgrading-0.37.md) diff --git a/packages/aws-rfdk/docs/upgrade/upgrading-0.37.md b/packages/aws-rfdk/docs/upgrade/upgrading-0.37.md new file mode 100644 index 000000000..6e8ffee68 --- /dev/null +++ b/packages/aws-rfdk/docs/upgrade/upgrading-0.37.md @@ -0,0 +1,55 @@ +# Upgrading to RFDK v0.37.x or Newer + +Starting in RFDK v0.37.0, the default for TLS between the render queue and its clients, which is configured using the `RenderQueueExternalTLSProps` interface that the `RenderQueue` construct takes as a part of its constructor props, is now set to be enabled. + +## Upgrading Farms Already Using TLS + +If you are already setting fields on the `RenderQueueExternalTLSProps` for the Render Queue, no action is required. Redeploying your render farm after upgrading your version of RFDK should have no effect. + +## Upgrading Farms Not Using TLS + +To upgrade your farm if it does not currently configure TLS for connections to the Render Queue, there are two options: + +1. [Migrating to TLS](#migrating-to-tls) (recommended) +1. [Preserving plain HTTP](#preserving-plain-http) + +### Migrating to TLS + +#### RenderQueue Changes + +Versions of RFDK prior to 0.37.0 had internal TLS between the load balancer and its backing services on by default. This is configurable with the `internalProtocol` field on the `RenderQueueTrafficEncryptionProps` interface. This default was left as-is, so upgrading RFDK will have no effect on the protocol those backing services were already using and they will not need to be replaced. The TLS being enabled by default is between the listener on the load balancer and any Deadline clients that are connecting to it, which is configurable with the `externalProtocol` property on the `RenderQueueTrafficEncryptionProps` interface. + +There will be a few new constructs deployed to your farm: +1. A `PrivateHostedZone` will be created if you do not supply your own. We set the default domain to `aws-rfdk.com`, which we have registered and suggest that you use if you do not have your own registered domain. [RFC 6762](https://datatracker.ietf.org/doc/html/rfc6762#appendix-G) recommends against using any unregistered top-level domains. +1. A self-signed X509 certificate will be generated using OpenSSL and that will then be used to sign a certificate that the Render Queue will use for TLS. Specifically, the certificate will be passed to the Application Listener for the Application Load Balancer that the Render Queue creates. Additional details about how RFDK uses TLS can the built-in certificate management can be found in the developer guide for [Encryption in transit](https://docs.aws.amazon.com/rfdk/latest/guide/security-encrypt-in-transit.html). + +These new constructs will require the Render Queue load balancer's listener to need replacing, but the load balancer itself and the backing services it redirects traffic to will not need to be changed. + +#### WorkerInstanceFleet and SpotEventPluginFleet Changes + +Since the endpoint and port the listener on the load balancer uses will be changed, and the TLS will require any clients connecting to verify its certificate, any stacks that contain dependencies on the Render Queue will first need to be destroyed. If you are using a tiered architecture similar to what we recommend in our documentation, this would include any `WorkerInstanceFleet` constructs or `SpotEventPluginFleet` and `ConfigureSpotEventPlugin` constructs. If you are not using a tiered architecture, we still recommend destroying these constructs since we're changing the endpoint that they need to connect to, and that configuration of the endpoint happens in the initialization script for an instance. + +To perform the removal of these constructs: +1. Suspend any jobs that are being run by workers that the constructs deployed. +2. If you are using the `ConfigureSpotEventPlugin` and `SpotEventPluginFleet` constructs, then any spot fleets launched by the Spot Event Plugin will need to be terminated, since their lifecycle is controlled by Deadline's Spot Event Plugin and not your RFDK app. Trying to destroy/remove the `SpotEventPluginFleet` construct will fail if these hosts are left running because the spot instances use the security group that the construct creates. +3. Next we have to destroy the constructs that are deploying workers, which could be done in a few ways: + 1. If you can destroy the Stack that contains these constructs without destroying the rest of your app, then destroy it using the command `cdk destroy "ComputeTier"` (or whatever name you gave your stack). + 2. If you cannot destroy a single stack or you are not using a tiered architecture, you can just comment out these constructs in your app, rebuild it, and then run `cdk deploy "*"` to perform the removal. +4. Now that we won't cause any dependency issues, we can upgrade the version of RFDK and then run the deployment. If worker constructs were commented out in the last step, they can be added back in here and redeployed during the upgrade. +5. Any jobs that were paused can be resumed after the deployment is complete. Any workers deployed with the `WorkerInstanceFleet` should be connecting through TLS now, and the Spot Event Plugin should be configured so that any new Spot instances it deploys will be properly configured as well. + +### Preserving plain HTTP + +While we strongly suggest farms be upgraded to use TLS, it is possible to override the new default and keep a farm using HTTP instead. To do this, there is an `enabled` field on the `RenderQueueExternalTLSProps` that can be set to false. This will prevent the farm from automatically upgrading the protocol until you decide you're ready. Here's an example of creating a Render Queue with TLS disabled: + +```ts +new RenderQueue(this, 'RenderQueue', { + vpc, + images, + repository, + version, + trafficEncryption: { + externalTLS: { enabled: false }, + }, +}); +``` diff --git a/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts b/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts index cabe872fb..7059ac176 100644 --- a/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts +++ b/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts @@ -56,7 +56,10 @@ export interface RenderQueueHostNameProps { readonly hostname?: string; /** - * The private zone to which the DNS A record for the render queue will be added. + * The private zone to which the DNS A record for the render queue will be added. We do not recommend + * using an unregistered domain for your PrivateHostedZone and we have registered aws-rfdk.com that + * can be used if you do not own your own. Refer to RFC 6762 Appendix G for more details about private + * DNS namespaces: https://datatracker.ietf.org/doc/html/rfc6762#appendix-G */ readonly zone: IPrivateHostedZone; } @@ -148,9 +151,15 @@ export interface RenderQueueHealthCheckConfiguration { * In both cases the certificate chain **must** include only the CA certificates PEM file due to a known limitation in Deadline. */ export interface RenderQueueExternalTLSProps { + /** + * Whether to enable TLS between the Render Queue and Deadline clients. + * @default true + */ + readonly enabled?: boolean; + /** * The ACM certificate that will be used for establishing incoming external TLS connections to the RenderQueue. - * @default If not provided then the rfdkCertificate must be provided. + * @default If rfdkCertificate and acmCertificate are both not provided when TLS is enabled, an rfdkCertificate will be generated and used. */ readonly acmCertificate?: ICertificate; @@ -166,7 +175,7 @@ export interface RenderQueueExternalTLSProps { /** * The parameters for an X509 Certificate that will be imported into ACM then used by the RenderQueue. * - * @default If not provided then an acmCertificate and acmCertificateChain must be provided. + * @default If rfdkCertificate and acmCertificate are both not provided when TLS is enabled, an rfdkCertificate will be generated and used. */ readonly rfdkCertificate?: IX509CertificatePem; } @@ -281,7 +290,7 @@ export interface RenderQueueProps { /** * Hostname to use to connect to the RenderQueue. * - * @default A hostname is generated by the Application Load Balancer that fronts the RenderQueue. + * @default - The hostname `renderqueue` will be used and a PrivateHostedZone will be created with the domain name `aws-rfdk.com` */ readonly hostname?: RenderQueueHostNameProps; diff --git a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts index e5c80bbe9..6a15ec66d 100644 --- a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts +++ b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts @@ -19,6 +19,7 @@ import { IConnectable, InstanceType, ISecurityGroup, + IVpc, Port, SubnetType, } from '@aws-cdk/aws-ec2'; @@ -50,6 +51,7 @@ import { import { ILogGroup, } from '@aws-cdk/aws-logs'; +import { IHostedZone, IPrivateHostedZone, PrivateHostedZone } from '@aws-cdk/aws-route53'; import { ISecret, } from '@aws-cdk/aws-secretsmanager'; @@ -64,6 +66,8 @@ import { InstanceConnectOptions, IRepository, IVersion, + RenderQueueExternalTLSProps, + RenderQueueHostNameProps, RenderQueueProps, RenderQueueSizeConstraints, VersionQuery, @@ -109,6 +113,32 @@ export interface IRenderQueue extends IConstruct, IConnectable { configureClientInstance(params: InstanceConnectOptions): void; } +/** + * Interface for information about the render queue's TLS configuration + */ +interface TlsInfo { + /** + * The certificate the Render Queue's server will use for the TLS connection. + */ + readonly serverCert: ICertificate; + + /** + * The certificate chain clients can use to verify the certificate. + */ + readonly certChain: ISecret; + + /** + * The private hosted zone that the render queue's load balancer will be placed in. + */ + readonly domainZone: IPrivateHostedZone; + + /** + * The fully qualified domain name that will be given to the load balancer in the private + * hosted zone. + */ + readonly fullyQualifiedDomainName: string; +} + /** * Base class for Render Queue providers */ @@ -162,7 +192,7 @@ abstract class RenderQueueBase extends Construct implements IRenderQueue { * should be governed carefully, as malicious software could use the API to remotely execute code across the entire render farm. * - The RenderQueue can be deployed with network encryption through Transport Layer Security (TLS) or without it. Unencrypted * network communications can be eavesdropped upon or modified in transit. We strongly recommend deploying the RenderQueue - * with TLS enabled in production environments. + * with TLS enabled in production environments and it is configured to be on by default. */ export class RenderQueue extends RenderQueueBase implements IGrantable { /** @@ -173,6 +203,10 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { [ApplicationProtocol.HTTPS]: 4433, }; + private static readonly DEFAULT_HOSTNAME = 'renderqueue'; + + private static readonly DEFAULT_DOMAIN_NAME = 'aws-rfdk.com'; + /** * The minimum Deadline version required for the Remote Connection Server to support load-balancing */ @@ -295,38 +329,28 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { this.version = props?.version; - let externalProtocol: ApplicationProtocol; - if ( props.trafficEncryption?.externalTLS ) { - externalProtocol = ApplicationProtocol.HTTPS; - - if ( (props.trafficEncryption.externalTLS.acmCertificate === undefined ) === - (props.trafficEncryption.externalTLS.rfdkCertificate === undefined) ) { - throw new Error('Exactly one of externalTLS.acmCertificate and externalTLS.rfdkCertificate must be provided when using externalTLS.'); - } else if (props.trafficEncryption.externalTLS.rfdkCertificate ) { - if (props.trafficEncryption.externalTLS.rfdkCertificate.certChain === undefined) { - throw new Error('Provided rfdkCertificate does not contain a certificate chain.'); - } - this.clientCert = new ImportedAcmCertificate(this, 'AcmCert', props.trafficEncryption.externalTLS.rfdkCertificate ); - this.certChain = props.trafficEncryption.externalTLS.rfdkCertificate.certChain; - } else { - if (props.trafficEncryption.externalTLS.acmCertificateChain === undefined) { - throw new Error('externalTLS.acmCertificateChain must be provided when using externalTLS.acmCertificate.'); - } - this.clientCert = props.trafficEncryption.externalTLS.acmCertificate; - this.certChain = props.trafficEncryption.externalTLS.acmCertificateChain; - } + const externalProtocol = props.trafficEncryption?.externalTLS?.enabled === false ? ApplicationProtocol.HTTP : ApplicationProtocol.HTTPS; + let loadBalancerFQDN: string | undefined; + let domainZone: IHostedZone | undefined; + + if ( externalProtocol === ApplicationProtocol.HTTPS ) { + const tlsInfo = this.getOrCreateTlsInfo(props); + + this.certChain = tlsInfo.certChain; + this.clientCert = tlsInfo.serverCert; + loadBalancerFQDN = tlsInfo.fullyQualifiedDomainName; + domainZone = tlsInfo.domainZone; } else { - externalProtocol = ApplicationProtocol.HTTP; + if (props.hostname) { + loadBalancerFQDN = this.generateFullyQualifiedDomainName(props.hostname.zone, props.hostname.hostname); + domainZone = props.hostname.zone; + } } this.version = props.version; const internalProtocol = props.trafficEncryption?.internalProtocol ?? ApplicationProtocol.HTTPS; - if (externalProtocol === ApplicationProtocol.HTTPS && !props.hostname) { - throw new Error('A hostname must be provided when the external protocol is HTTPS'); - } - this.cluster = new Cluster(this, 'Cluster', { vpc: props.vpc, }); @@ -390,14 +414,6 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { this.taskDefinition = taskDefinition; // The fully-qualified domain name to use for the ALB - let loadBalancerFQDN: string | undefined; - if (props.hostname) { - const label = props.hostname.hostname ?? 'renderqueue'; - if (props.hostname.hostname && !RenderQueue.RE_VALID_HOSTNAME.test(label)) { - throw new Error(`Invalid RenderQueue hostname: ${label}`); - } - loadBalancerFQDN = `${label}.${props.hostname.zone.zoneName}`; - } const loadBalancer = new ApplicationLoadBalancer(this, 'LB', { vpc: this.cluster.vpc, @@ -411,7 +427,7 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { certificate: this.clientCert, cluster: this.cluster, desiredCount: this.renderQueueSize?.desired, - domainZone: props.hostname?.zone, + domainZone, domainName: loadBalancerFQDN, listenerPort: externalPortNumber, loadBalancer, @@ -692,4 +708,118 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { return taskDefinition; } + + /** + * Checks if the user supplied any certificate to use for TLS and uses them, or creates defaults to use. + * @param props + * @returns TlsInfo either based on input to the render queue, or the created defaults + */ + private getOrCreateTlsInfo(props: RenderQueueProps): TlsInfo { + if ( (props.trafficEncryption?.externalTLS?.acmCertificate !== undefined ) || + (props.trafficEncryption?.externalTLS?.rfdkCertificate !== undefined) ) { + if (props.hostname === undefined) { + throw new Error('The hostname for the render queue must be defined if supplying your own certificates.'); + } + return this.getTlsInfoFromUserProps( + props.trafficEncryption.externalTLS, + props.hostname, + ); + } + + return this.createDefaultTlsInfo(props.vpc, props.hostname); + } + + /** + * Creates a default certificate to use for TLS and a PrivateHostedZone to put the load balancer in. + * @param vpc + * @param hostname + * @returns default TlsInfo + */ + private createDefaultTlsInfo(vpc: IVpc, hostname?: RenderQueueHostNameProps) { + const domainZone = hostname?.zone ?? new PrivateHostedZone(this, 'DnsZone', { + vpc: vpc, + zoneName: RenderQueue.DEFAULT_DOMAIN_NAME, + }); + + const fullyQualifiedDomainName = this.generateFullyQualifiedDomainName(domainZone, hostname?.hostname); + + const rootCa = new X509CertificatePem(this, 'RootCA', { + subject: { + cn: 'RenderQueueRootCA', + }, + }); + const rfdkCert = new X509CertificatePem(this, 'RenderQueuePemCert', { + subject: { + cn: fullyQualifiedDomainName, + }, + signingCertificate: rootCa, + }); + const serverCert = new ImportedAcmCertificate( this, 'AcmCert', rfdkCert ); + const certChain = rfdkCert.certChain!; + + return { + domainZone, + fullyQualifiedDomainName, + serverCert, + certChain, + }; + } + + /** + * Gets the certificate and PrivateHostedZone provided in the Render Queue's construct props. + * @param externalTLS + * @param hostname + * @returns The provided certificate and domain info + */ + private getTlsInfoFromUserProps(externalTLS: RenderQueueExternalTLSProps, hostname: RenderQueueHostNameProps): TlsInfo { + let serverCert: ICertificate; + let certChain: ISecret; + + if ( (externalTLS.acmCertificate !== undefined ) && + (externalTLS.rfdkCertificate !== undefined) ) { + throw new Error('Exactly one of externalTLS.acmCertificate and externalTLS.rfdkCertificate must be provided when using externalTLS.'); + } + + if (!hostname.hostname) { + throw new Error('A hostname must be supplied if a certificate is supplied, ' + + 'with the common name of the certificate matching the hostname + domain name.'); + } + + const fullyQualifiedDomainName = this.generateFullyQualifiedDomainName(hostname.zone, hostname.hostname); + + if ( externalTLS.acmCertificate ) { + if ( externalTLS.acmCertificateChain === undefined ) { + throw new Error('externalTLS.acmCertificateChain must be provided when using externalTLS.acmCertificate.'); + } + serverCert = externalTLS.acmCertificate; + certChain = externalTLS.acmCertificateChain; + + } else { // Using externalTLS.rfdkCertificate + if ( externalTLS.rfdkCertificate!.certChain === undefined ) { + throw new Error('Provided rfdkCertificate does not contain a certificate chain.'); + } + serverCert = new ImportedAcmCertificate( this, 'AcmCert', externalTLS.rfdkCertificate! ); + certChain = externalTLS.rfdkCertificate!.certChain; + } + + return { + domainZone: hostname.zone, + fullyQualifiedDomainName, + serverCert, + certChain, + }; + } + + /** + * Helper method to create the fully qualified domain name for the given hostname and PrivateHostedZone. + * @param hostname + * @param zone + * @returns The fully qualified domain name + */ + private generateFullyQualifiedDomainName(zone: IPrivateHostedZone, hostname: string = RenderQueue.DEFAULT_HOSTNAME): string { + if (!RenderQueue.RE_VALID_HOSTNAME.test(hostname)) { + throw new Error(`Invalid RenderQueue hostname: ${hostname}`); + } + return `${hostname}.${zone.zoneName}`; + } } diff --git a/packages/aws-rfdk/lib/deadline/test/configure-spot-event-plugin.test.ts b/packages/aws-rfdk/lib/deadline/test/configure-spot-event-plugin.test.ts index b7a513edb..7566635ee 100644 --- a/packages/aws-rfdk/lib/deadline/test/configure-spot-event-plugin.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/configure-spot-event-plugin.test.ts @@ -86,6 +86,7 @@ describe('ConfigureSpotEventPlugin', () => { vpc, version, }), + trafficEncryption: { externalTLS: { enabled: false } }, version, }); @@ -688,6 +689,7 @@ describe('ConfigureSpotEventPlugin', () => { vpc, version, }), + trafficEncryption: { externalTLS: { enabled: false } }, version, }); @@ -909,6 +911,7 @@ describe('ConfigureSpotEventPlugin', () => { vpc, version, }), + trafficEncryption: { externalTLS: { enabled: false } }, version, }); @@ -943,6 +946,7 @@ describe('ConfigureSpotEventPlugin', () => { vpc, version, }), + trafficEncryption: { externalTLS: { enabled: false } }, version, }); diff --git a/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts b/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts index 3da4d0ae2..94065e887 100644 --- a/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts @@ -6,6 +6,7 @@ import { ABSENT, arrayWith, + countResources, countResourcesLike, deepObjectLike, expect as expectCDK, @@ -53,10 +54,12 @@ import { Secret } from '@aws-cdk/aws-secretsmanager'; import { App, CfnElement, + CustomResource, Stack, } from '@aws-cdk/core'; import { + ImportedAcmCertificate, X509CertificatePem, } from '../..'; import { @@ -490,10 +493,10 @@ describe('RenderQueue', () => { })); }); - test('to HTTP externally between clients and ALB', () => { + test('to HTTPS externally between clients and ALB', () => { expectCDK(isolatedStack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { - Protocol: 'HTTP', - Port: 8080, + Protocol: 'HTTPS', + Port: 4433, })); }); }); @@ -642,6 +645,7 @@ describe('RenderQueue', () => { vpc, trafficEncryption: { internalProtocol: ApplicationProtocol.HTTP, + externalTLS: { enabled: false }, }, }; @@ -683,10 +687,11 @@ describe('RenderQueue', () => { trafficEncryption: { externalTLS: { acmCertificate: Certificate.fromCertificateArn(stack, 'Certificate', CERT_ARN), - acmCertificateChain: Secret.fromSecretArn(stack, 'CA_Cert', CA_ARN), + acmCertificateChain: Secret.fromSecretPartialArn(stack, 'CA_Cert', CA_ARN), }, }, hostname: { + hostname: 'renderqueue', zone, }, }; @@ -718,7 +723,7 @@ describe('RenderQueue', () => { })); }); - test('raises an error when a cert is specified without a hostname', () => { + test('raises an error when a cert is specified without a hosted zone', () => { // GIVEN const props: RenderQueueProps = { images, @@ -738,99 +743,232 @@ describe('RenderQueue', () => { new RenderQueue(stack, 'RenderQueue', props); }) // THEN - .toThrow(/A hostname must be provided when the external protocol is HTTPS/); + .toThrow(/The hostname for the render queue must be defined if supplying your own certificates./); }); - }); - describe('externalProtocol is HTTPS importing cert', () => { - let isolatedStack: Stack; - let zone: PrivateHostedZone; - const ZONE_NAME = 'renderfarm.local'; - - beforeEach(() => { + test('raises an error when a cert is specified without a hostname', () => { // GIVEN - isolatedStack = new Stack(app, 'IsolatedStack'); - zone = new PrivateHostedZone(isolatedStack, 'RenderQueueZone', { + const zone = new PrivateHostedZone(isolatedStack, 'RenderQueueZoneNoName', { vpc, zoneName: ZONE_NAME, }); - const caCert = new X509CertificatePem(isolatedStack, 'CaCert', { - subject: { - cn: `ca.${ZONE_NAME}`, - }, - }); - const serverCert = new X509CertificatePem(isolatedStack, 'ServerCert', { - subject: { - cn: `server.${ZONE_NAME}`, - }, - signingCertificate: caCert, - }); - const props: RenderQueueProps = { images, repository, - version: new VersionQuery(isolatedStack, 'Version'), + version: renderQueueVersion, vpc, trafficEncryption: { externalTLS: { - rfdkCertificate: serverCert, + acmCertificate: Certificate.fromCertificateArn(stack, 'Cert', 'certArn'), + acmCertificateChain: Secret.fromSecretArn(stack, 'CA_Cert2', CA_ARN), }, - internalProtocol: ApplicationProtocol.HTTP, - }, - hostname: { - zone, }, + hostname: { zone }, }; // WHEN - new RenderQueue(isolatedStack, 'RenderQueue', props); + expect(() => { + new RenderQueue(stack, 'RenderQueue', props); + }) + // THEN + .toThrow(/A hostname must be supplied if a certificate is supplied, with the common name of the certificate matching the hostname \+ domain name/); }); + }); - test('sets the listener port to 4433', () => { - // THEN - expectCDK(isolatedStack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { - Port: 4433, - })); - }); + describe('externalProtocol is HTTPS importing cert', () => { + describe('passing cases', () => { + let isolatedStack: Stack; + let zone: PrivateHostedZone; + const ZONE_NAME = 'renderfarm.local'; + const HOSTNAME = 'server'; + + beforeEach(() => { + // GIVEN + isolatedStack = new Stack(app, 'IsolatedStack'); + zone = new PrivateHostedZone(isolatedStack, 'RenderQueueZone', { + vpc, + zoneName: ZONE_NAME, + }); + + const caCert = new X509CertificatePem(isolatedStack, 'CaCert', { + subject: { + cn: `ca.${ZONE_NAME}`, + }, + }); + const serverCert = new X509CertificatePem(isolatedStack, 'ServerCert', { + subject: { + cn: `${HOSTNAME}.${ZONE_NAME}`, + }, + signingCertificate: caCert, + }); + + const props: RenderQueueProps = { + images, + repository, + version: new VersionQuery(isolatedStack, 'Version'), + vpc, + trafficEncryption: { + externalTLS: { + rfdkCertificate: serverCert, + }, + internalProtocol: ApplicationProtocol.HTTP, + }, + hostname: { + zone, + hostname: HOSTNAME, + }, + }; - test('sets the listener protocol to HTTPS', () => { - // THEN - expectCDK(isolatedStack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { - Protocol: 'HTTPS', - })); - }); + // WHEN + new RenderQueue(isolatedStack, 'RenderQueue', props); + }); - test('Imports Cert to ACM', () => { - // THEN - expectCDK(isolatedStack).to(haveResourceLike('Custom::RFDK_AcmImportedCertificate', { - X509CertificatePem: { - Cert: { - 'Fn::GetAtt': [ - 'ServerCert', - 'Cert', - ], + test('sets the listener port to 4433', () => { + // THEN + expectCDK(isolatedStack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Port: 4433, + })); + }); + + test('sets the listener protocol to HTTPS', () => { + // THEN + expectCDK(isolatedStack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Protocol: 'HTTPS', + })); + }); + + test('Imports Cert to ACM', () => { + // THEN + expectCDK(isolatedStack).to(haveResourceLike('Custom::RFDK_AcmImportedCertificate', { + X509CertificatePem: { + Cert: { + 'Fn::GetAtt': [ + 'ServerCert', + 'Cert', + ], + }, + Key: { + 'Fn::GetAtt': [ + 'ServerCert', + 'Key', + ], + }, + Passphrase: { + Ref: 'ServerCertPassphraseE4C3CB38', + }, + CertChain: { + 'Fn::GetAtt': [ + 'ServerCert', + 'CertChain', + ], + }, }, - Key: { - 'Fn::GetAtt': [ - 'ServerCert', - 'Key', - ], + })); + }); + }); + + describe('failure cases,', () => { + test('Throws when missing cert chain', () => { + const ZONE_NAME = 'renderfarm.local'; + const HOSTNAME = 'server'; + // GIVEN + const isolatedStack = new Stack(app, 'IsolatedStack'); + const zone = new PrivateHostedZone(isolatedStack, 'RenderQueueZone', { + vpc, + zoneName: ZONE_NAME, + }); + + const rootCert = new X509CertificatePem(isolatedStack, 'RootCert', { + subject: { + cn: `ca.${ZONE_NAME}`, }, - Passphrase: { - Ref: 'ServerCertPassphraseE4C3CB38', + }); + + const props: RenderQueueProps = { + images, + repository, + version: new VersionQuery(isolatedStack, 'Version'), + vpc, + trafficEncryption: { + externalTLS: { + rfdkCertificate: rootCert, + }, + internalProtocol: ApplicationProtocol.HTTP, }, - CertChain: { - 'Fn::GetAtt': [ - 'ServerCert', - 'CertChain', - ], + hostname: { + zone, + hostname: HOSTNAME, }, - }, - })); + }; + + // WHEN + expect(() => { + new RenderQueue(isolatedStack, 'RenderQueue', props); + }) + // THEN + .toThrow(/Provided rfdkCertificate does not contain a certificate chain/); + }); }); }); + test('Creates default RFDK cert if no cert given', () => { + // GIVEN + const isolatedStack = new Stack(app, 'IsolatedStack'); + + const props: RenderQueueProps = { + images, + repository, + version: new VersionQuery(isolatedStack, 'Version'), + vpc, + trafficEncryption: { + externalTLS: { + }, + }, + }; + + const rq = new RenderQueue(isolatedStack, 'RenderQueue', props); + + const rootCa = rq.node.findChild('RootCA').node.defaultChild as X509CertificatePem; + const rootCaGen = rootCa.node.defaultChild as CustomResource; + const rfdkCert = rq.node.findChild('RenderQueuePemCert').node.defaultChild as X509CertificatePem; + const rfdkCertGen = rfdkCert.node.defaultChild as CustomResource; + const acmCert = rq.node.findChild('AcmCert').node.defaultChild as ImportedAcmCertificate; + + expectCDK(isolatedStack).to(haveResourceLike('Custom::RFDK_X509Generator', { + Passphrase: isolatedStack.resolve(rootCa.passphrase), + })); + + expectCDK(isolatedStack).to(haveResourceLike('Custom::RFDK_X509Generator', { + Passphrase: isolatedStack.resolve(rfdkCert.passphrase), + SigningCertificate: { + Cert: isolatedStack.resolve(rootCaGen.getAtt('Cert')), + Key: isolatedStack.resolve(rootCaGen.getAtt('Key')), + Passphrase: isolatedStack.resolve(rootCa.passphrase), + CertChain: '', + }, + })); + + expectCDK(isolatedStack).to(countResources('Custom::RFDK_AcmImportedCertificate', 1)); + expectCDK(isolatedStack).to(haveResourceLike('Custom::RFDK_AcmImportedCertificate', { + X509CertificatePem: { + Cert: isolatedStack.resolve(rfdkCertGen.getAtt('Cert')), + Key: isolatedStack.resolve(rfdkCertGen.getAtt('Key')), + Passphrase: isolatedStack.resolve(rfdkCert.passphrase), + CertChain: isolatedStack.resolve(rfdkCertGen.getAtt('CertChain')), + }, + })); + + expectCDK(isolatedStack).to(countResources('AWS::ElasticLoadBalancingV2::Listener', 1)); + expectCDK(isolatedStack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Certificates: [ + { + CertificateArn: isolatedStack.resolve(acmCert.certificateArn), + }, + ], + })); + }); + test('Throws if given ACM cert and RFDK Cert', () => { // GIVEN const isolatedStack = new Stack(app, 'IsolatedStack'); @@ -880,41 +1018,10 @@ describe('RenderQueue', () => { .toThrow(/Exactly one of externalTLS.acmCertificate and externalTLS.rfdkCertificate must be provided when using externalTLS/); }); - test('Throws if no Cert given', () => { - // GIVEN - const isolatedStack = new Stack(app, 'IsolatedStack'); - const ZONE_NAME = 'renderfarm.local'; - - const zone = new PrivateHostedZone(isolatedStack, 'RenderQueueZone', { - vpc, - zoneName: ZONE_NAME, - }); - - const props: RenderQueueProps = { - images, - repository, - version: new VersionQuery(isolatedStack, 'Version'), - vpc, - trafficEncryption: { - externalTLS: { - }, - }, - hostname: { - zone, - }, - }; - - // WHEN - expect(() => { - new RenderQueue(isolatedStack, 'RenderQueue', props); - }) - // THEN - .toThrow(/Exactly one of externalTLS.acmCertificate and externalTLS.rfdkCertificate must be provided when using externalTLS/); - }); - test('Throws if ACM Cert is given without a cert chain', () => { // GIVEN const isolatedStack = new Stack(app, 'IsolatedStack'); + const HOSTNAME = 'renderqueue'; const ZONE_NAME = 'renderfarm.local'; const CERT_ARN = 'certArn'; @@ -934,6 +1041,7 @@ describe('RenderQueue', () => { }, }, hostname: { + hostname: HOSTNAME, zone, }, }; @@ -969,6 +1077,7 @@ describe('RenderQueue', () => { hostname: { zone, }, + trafficEncryption: { externalTLS: { enabled: false } }, }; // WHEN @@ -1365,6 +1474,7 @@ describe('RenderQueue', () => { let isolatedStack: Stack; let zone: PrivateHostedZone; let rq: RenderQueue; + const HOSTNAME = 'renderqueue'; const ZONE_NAME = 'renderfarm.local'; const CERT_ARN = 'arn:a:b:c:dcertarn'; const CA_ARN = 'arn:aws:secretsmanager:123456789012:secret:ca/arn'; @@ -1382,6 +1492,7 @@ describe('RenderQueue', () => { version: new VersionQuery(isolatedStack, 'Version'), vpc, hostname: { + hostname: HOSTNAME, zone, }, trafficEncryption: { @@ -1881,7 +1992,7 @@ describe('RenderQueue', () => { // GIVEN const zoneName = 'mydomain.local'; - describe('not specified', () => { + describe('not specified, with no TLS', () => { let isolatedStack: Stack; beforeEach(() => { @@ -1890,6 +2001,7 @@ describe('RenderQueue', () => { const props: RenderQueueProps = { images, repository, + trafficEncryption: { externalTLS: { enabled: false } }, version: new VersionQuery(isolatedStack, 'Version'), vpc, }; @@ -1904,6 +2016,32 @@ describe('RenderQueue', () => { }); }); + test('not specified, with TLS', () => { + // GIVEN + const isolatedStack = new Stack(app, 'IsolatedStack'); + + const props: RenderQueueProps = { + images, + repository, + version: new VersionQuery(isolatedStack, 'Version'), + vpc, + trafficEncryption: { + externalTLS: { + }, + }, + }; + + const renderQueue = new RenderQueue(isolatedStack, 'RenderQueue', props); + + expectCDK(isolatedStack).to(haveResource('AWS::Route53::RecordSet', { + Name: 'renderqueue.aws-rfdk.com.', + Type: 'A', + AliasTarget: objectLike({ + HostedZoneId: isolatedStack.resolve(renderQueue.loadBalancer.loadBalancerCanonicalHostedZoneId), + }), + })); + }); + describe('specified with zone but no hostname', () => { let zone: PrivateHostedZone; let isolatedStack: Stack; @@ -1950,6 +2088,52 @@ describe('RenderQueue', () => { }); }); + test.each([ + [false], + [true], + ])('specified with TLS enabled == %s', (isTlsEnabled: boolean) => { + // GIVEN + const zone = new PrivateHostedZone(dependencyStack, 'Zone', { + vpc, + zoneName, + }); + const hostname = 'testrq'; + const isolatedStack = new Stack(app, 'IsolatedStack'); + const props: RenderQueueProps = { + images, + repository, + version: new VersionQuery(isolatedStack, 'Version'), + vpc, + hostname: { + hostname, + zone, + }, + trafficEncryption: { + externalTLS: { enabled: isTlsEnabled }, + }, + }; + + // WHEN + const renderQueue = new RenderQueue(isolatedStack, 'RenderQueue', props); + + // THEN + const loadBalancerLogicalId = dependencyStack.getLogicalId( + renderQueue.loadBalancer.node.defaultChild as CfnElement, + ); + expectCDK(isolatedStack).to(haveResource('AWS::Route53::RecordSet', { + Name: `${hostname}.${zoneName}.`, + Type: 'A', + AliasTarget: objectLike({ + HostedZoneId: { + 'Fn::GetAtt': [ + loadBalancerLogicalId, + 'CanonicalHostedZoneID', + ], + }, + }), + })); + }); + test.each([ ['rq.somedomain.local'], ['1startswithnumber'], @@ -2271,13 +2455,13 @@ describe('RenderQueue', () => { resourceTypeCounts: { 'AWS::ECS::Cluster': 1, 'AWS::EC2::SecurityGroup': 2, - 'AWS::IAM::Role': 8, + 'AWS::IAM::Role': 10, 'AWS::AutoScaling::AutoScalingGroup': 1, - 'AWS::Lambda::Function': 4, + 'AWS::Lambda::Function': 6, 'AWS::SNS::Topic': 1, 'AWS::ECS::TaskDefinition': 1, - 'AWS::DynamoDB::Table': 2, - 'AWS::SecretsManager::Secret': 2, + 'AWS::DynamoDB::Table': 5, + 'AWS::SecretsManager::Secret': 4, 'AWS::ElasticLoadBalancingV2::LoadBalancer': 1, 'AWS::ElasticLoadBalancingV2::TargetGroup': 1, 'AWS::ECS::Service': 1, diff --git a/packages/aws-rfdk/lib/deadline/test/spot-event-plugin-fleet.test.ts b/packages/aws-rfdk/lib/deadline/test/spot-event-plugin-fleet.test.ts index e0eea3857..d41860546 100644 --- a/packages/aws-rfdk/lib/deadline/test/spot-event-plugin-fleet.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/spot-event-plugin-fleet.test.ts @@ -100,6 +100,7 @@ describe('SpotEventPluginFleet', () => { vpc, version, }), + trafficEncryption: { externalTLS: { enabled: false } }, version, }); spotFleetStack = new Stack(app, 'SpotFleetStack', { diff --git a/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts b/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts index 5f5692b8e..fec8b95ba 100644 --- a/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts @@ -87,6 +87,7 @@ describe('UsageBasedLicensing', () => { vpc, version: versionedInstallers, }), + trafficEncryption: { externalTLS: { enabled: false } }, version: versionedInstallers, }); diff --git a/packages/aws-rfdk/lib/deadline/test/worker-configuration.test.ts b/packages/aws-rfdk/lib/deadline/test/worker-configuration.test.ts index c6b0d8cae..96140c6d0 100644 --- a/packages/aws-rfdk/lib/deadline/test/worker-configuration.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/worker-configuration.test.ts @@ -329,6 +329,7 @@ describe('Test WorkerInstanceConfiguration connect to RenderQueue', () => { vpc, version, }), + trafficEncryption: { externalTLS: { enabled: false } }, }); const rqSecGrp = renderQueue.connections.securityGroups[0] as SecurityGroup; renderQueueSGId = stack.resolve(rqSecGrp.securityGroupId); diff --git a/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts b/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts index a6d2ddf84..d7914d27a 100644 --- a/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts @@ -90,6 +90,7 @@ beforeEach(() => { vpc, version, }), + trafficEncryption: { externalTLS: { enabled: false } }, version, }); wfstack = new Stack(app, 'workerFleetStack', {