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

feat(deadline): use TLS between RenderQueue and clients by default #491

Merged
merged 6 commits into from
Jul 23, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -15,6 +15,8 @@
from aws_rfdk.deadline import (
AwsThinkboxEulaAcceptance,
RenderQueue,
RenderQueueExternalTLSProps,
RenderQueueTrafficEncryptionProps,
jusiskin marked this conversation as resolved.
Show resolved Hide resolved
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 ) ),
jusiskin marked this conversation as resolved.
Show resolved Hide resolved
)
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.
jusiskin marked this conversation as resolved.
Show resolved Hide resolved
*/
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.
jusiskin marked this conversation as resolved.
Show resolved Hide resolved
*/
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;
jusiskin marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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;
kozlove-aws marked this conversation as resolved.
Show resolved Hide resolved

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', {
jusiskin marked this conversation as resolved.
Show resolved Hide resolved
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