Skip to content

Commit

Permalink
feat(deadline): tls between render queue and clients enabled by default
Browse files Browse the repository at this point in the history
fixes #490

BREAKING CHANGE: Farms currently not configured to use external TLS on
the Render Queue will be modified to have it enabled and using the
default certificate and hosted zone. To continue to keep external TLS
disabled, the `enabled` flag on the `RenderQueueExternalTLSProps` can be
set to false; however, we strongly encourage you to enable TLS.
  • Loading branch information
horsmand committed Jun 29, 2021
1 parent 11e30f6 commit 42d1056
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 153 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from aws_rfdk.deadline import (
AwsThinkboxEulaAcceptance,
RenderQueue,
RenderQueueExternalTLSProps,
RenderQueueTrafficEncryptionProps,
Repository,
RepositoryRemovalPolicies,
ThinkboxDockerImages,
Expand Down Expand Up @@ -81,4 +83,5 @@ def __init__(self, scope: Construct, stack_id: str, *, props: BaseFarmStackProps
images=images,
repository=repository,
deletion_protection=False,
traffic_encryption=RenderQueueTrafficEncryptionProps( external_tls=RenderQueueExternalTLSProps( enabled=False ) ),
)
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class BaseFarmStack extends Stack {
images,
repository,
deletionProtection: false,
trafficEncryption: { externalTLS: { enabled: false } },
});
}
}
12 changes: 7 additions & 5 deletions integ/lib/render-struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { X509CertificatePem } from 'aws-rfdk';
import {
IRepository,
RenderQueue,
RenderQueueHostNameProps,
RenderQueueProps,
RenderQueueTrafficEncryptionProps,
ThinkboxDockerRecipes,
} from 'aws-rfdk/deadline';
import { ThinkboxDockerImageOverrides } from './ThinkboxDockerImageOverrides';
Expand Down Expand Up @@ -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' ) {
Expand All @@ -72,8 +74,8 @@ export class RenderStruct extends Construct {
},
signingCertificate: cacert,
}),
internalProtocol: ApplicationProtocol.HTTP,
},
internalProtocol: ApplicationProtocol.HTTP,
};
hostname = {
zone: new PrivateHostedZone(this, 'Zone', {
Expand All @@ -83,7 +85,7 @@ export class RenderStruct extends Construct {
hostname: host,
};
} else {
trafficEncryption = undefined;
trafficEncryption = { externalTLS: { enabled: false } };
hostname = undefined;
}

Expand Down
14 changes: 10 additions & 4 deletions packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,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 not provided, rfdkCertificate will be used.
*/
readonly acmCertificate?: ICertificate;

Expand All @@ -159,14 +165,14 @@ export interface RenderQueueExternalTLSProps {
*
* This certifiate chain **must** include only the CA Certificates PEM file.
*
* @default If an acmCertificate was provided then this must be provided, otherwise this is ignored.
* @default: If an acmCertificate was provided then this must be provided, otherwise this is ignored.
*/
readonly acmCertificateChain?: ISecret;

/**
* 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, an rfdkCertificate will be generated and used.
*/
readonly rfdkCertificate?: IX509CertificatePem;
}
Expand Down Expand Up @@ -281,7 +287,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: A private hosted host will be created and the default hostname will be used.
*/
readonly hostname?: RenderQueueHostNameProps;

Expand Down
209 changes: 173 additions & 36 deletions packages/aws-rfdk/lib/deadline/lib/render-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
IConnectable,
InstanceType,
ISecurityGroup,
IVpc,
Port,
SubnetType,
} from '@aws-cdk/aws-ec2';
Expand Down Expand Up @@ -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';
Expand All @@ -64,6 +66,8 @@ import {
InstanceConnectOptions,
IRepository,
IVersion,
RenderQueueExternalTLSProps,
RenderQueueHostNameProps,
RenderQueueProps,
RenderQueueSizeConstraints,
VersionQuery,
Expand Down Expand Up @@ -109,6 +113,42 @@ export interface IRenderQueue extends IConstruct, IConnectable {
configureClientInstance(params: InstanceConnectOptions): void;
}

/**
* Interface for information about the render queue's domain.
*/
interface DomainInfo {
/**
* 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;
}

/**
* Interface for information about the render queue's TLS configuration
*/
interface TlsInfo {
/**
* The certificate to use for the TLS connection.
*/
readonly clientCert: ICertificate;

/**
* The certificate chain clients can use to verify the certificate.
*/
readonly certChain: ISecret;

/**
* The information about the domain for the render queue.
*/
readonly domainInfo: DomainInfo;
}

/**
* Base class for Render Queue providers
*/
Expand Down Expand Up @@ -162,7 +202,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 {
/**
Expand All @@ -173,6 +213,10 @@ export class RenderQueue extends RenderQueueBase implements IGrantable {
[ApplicationProtocol.HTTPS]: 4433,
};

private static readonly DEFAULT_HOSTNAME = 'renderqueue';

private static readonly DEFAULT_DOMAIN_NAME = 'rfdk.internal';

/**
* The minimum Deadline version required for the Remote Connection Server to support load-balancing
*/
Expand Down Expand Up @@ -295,38 +339,23 @@ 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;
}
} else {
externalProtocol = ApplicationProtocol.HTTP;
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.clientCert;
loadBalancerFQDN = tlsInfo.domainInfo.fullyQualifiedDomainName;
domainZone = tlsInfo.domainInfo.domainZone;
}

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,
});
Expand Down Expand Up @@ -390,14 +419,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,
Expand All @@ -411,7 +432,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,
Expand Down Expand Up @@ -692,4 +713,120 @@ 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 label = hostname?.hostname ?? RenderQueue.DEFAULT_HOSTNAME;
const domainInfo = this.createDomainInfo(label, domainZone);

const rootCa = new X509CertificatePem(this, 'RootCA', {
subject: {
cn: 'RenderQueueRootCA',
},
});
const rfdkCert = new X509CertificatePem(this, 'RenderQueueCA', {
subject: {
cn: domainInfo.fullyQualifiedDomainName,
},
signingCertificate: rootCa,
});
const clientCert = new ImportedAcmCertificate(this, 'AcmCert', rfdkCert );
const certChain = rfdkCert.certChain!;

return {
domainInfo,
clientCert,
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 clientCert: 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 domainInfo = this.createDomainInfo(hostname.hostname, hostname.zone);

if ( externalTLS.acmCertificate ) {
if ( externalTLS.acmCertificateChain === undefined ) {
throw new Error('externalTLS.acmCertificateChain must be provided when using externalTLS.acmCertificate.');
}
clientCert = 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.');
}
clientCert = new ImportedAcmCertificate(this, 'AcmCert', externalTLS.rfdkCertificate! );
certChain = externalTLS.rfdkCertificate!.certChain;
}

return {
domainInfo,
clientCert,
certChain,
};
}

/**
* Helper method to create the fully qualified domain name for the given hostname and PrivateHostedZone.
* @param hostname
* @param zone
* @returns DomainInfo containing the PrivateHostedZone and fully qualified domain name
*/
private createDomainInfo(hostname: string, zone: IPrivateHostedZone): DomainInfo {
if (!RenderQueue.RE_VALID_HOSTNAME.test(hostname)) {
throw new Error(`Invalid RenderQueue hostname: ${hostname}`);
}

return {
domainZone: zone,
fullyQualifiedDomainName: `${hostname}.${zone.zoneName}`,
};
}
}
Loading

0 comments on commit 42d1056

Please sign in to comment.