Skip to content

Commit

Permalink
feat: full cloudfront stack
Browse files Browse the repository at this point in the history
  • Loading branch information
marnixdessing committed Apr 9, 2022
1 parent 96a5faf commit 01c4702
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 58 deletions.
1 change: 0 additions & 1 deletion src/ApiStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ export class ApiStack extends cdk.Stack {
});



// Modify the default api gateway unauhtorized response
// api.addGatewayResponse('basic-auth-unauthorized', {
// statusCode: '401',
Expand Down
2 changes: 1 addition & 1 deletion src/AppStage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class AppStage extends cdk.Stage {

const cloudfrontStack = new CloudFrontStack(this, 'cloud-front-stack', {
branch: props.branch,
apiGatewayDomain: apiStack.getApiGatewayDomain(),
hostDomain: apiStack.getApiGatewayDomain(),
});
cloudfrontStack.addDependency(apiStack);
cloudfrontStack.addDependency(dnsStack);
Expand Down
203 changes: 147 additions & 56 deletions src/CloudFrontStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,65 +5,112 @@ import {
aws_route53 as Route53,
aws_route53_targets as Route53Targets,
aws_ssm as SSM,
aws_s3 as S3,
aws_s3_deployment,
aws_iam as IAM,
} from 'aws-cdk-lib';
import {
Distribution,
PriceClass,
OriginRequestPolicy,
ViewerProtocolPolicy,
AllowedMethods,
ResponseHeadersPolicy,
HeadersFrameOption,
HeadersReferrerPolicy,
CachePolicy,
OriginRequestHeaderBehavior,
CacheCookieBehavior,
CacheHeaderBehavior,
CacheQueryStringBehavior,
SecurityPolicyProtocol,
OriginAccessIdentity,
} from 'aws-cdk-lib/aws-cloudfront';
import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
import { Statics } from './Statics';

export interface CloudFrontStackProps extends StackProps {
/**
* current branch: Determines subdomain of csp-nijmegen.nl
*/
branch: string;
* Domain for the default origin (HTTPorigin)
*/
hostDomain: string;
/**
* Where the api gateway lives
*/
apiGatewayDomain: string;
* current branch: Determines subdomain of csp-nijmegen.nl
*/
branch: string;
}

export class CloudFrontStack extends Stack {
constructor(scope: Construct, id: string, props: CloudFrontStackProps) {
super(scope, id);

//const apiGatewayUrlStr = SSM.StringParameter.fromStringParameterName(this, 'api-gateway-url', Statics.ssmApiGatewayUrl).stringValue;
//const apiGatewayUrl = new URL(apiGatewayUrlStr);
// const subdomain = Statics.subDomain(props.branch);
// const cspDomain = `${subdomain}.csp-nijmegen.nl`;
// const mainDomain = `${subdomain}.nijmegen.nl`;
// const domains = [cspDomain, mainDomain];

//const subdomain = Statics.subDomain(props.branch);
//const cspDomain = `${subdomain}.csp-nijmegen.nl`;
//const domains = [cspDomain];
// const certificateArn = this.certificateArn();

const cloudfrontDistribution = this.setCloudfrontStack(props.apiGatewayDomain);
const cloudfrontDistribution = this.setCloudfrontStack(props.hostDomain);
this.addDnsRecords(cloudfrontDistribution);
}

/**
* Create a cloudfront distribution for the application
*
* Do not forward the Host header to API Gateway. This results in
* an HTTP 403 because API Gateway won't be able to find an endpoint
* on the cloudfront domain.
*
* @param {string} apiGatewayDomain the domain the api gateway can be reached at
* @returns {Distribution} the cloudfront distribution
*/
setCloudfrontStack(apiGatewayDomain: string): Distribution { //, certificateArn?: string
* Create a cloudfront distribution for the application
*
* Do not forward the Host header to API Gateway. This results in
* an HTTP 403 because API Gateway won't be able to find an endpoint
* on the cloudfront domain.
*
* @param {string} apiGatewayDomain the domain the api gateway can be reached at
* @returns {Distribution} the cloudfront distribution
*/
setCloudfrontStack(apiGatewayDomain: string): Distribution {

const distribution = new Distribution(this, 'cf-distribution', {
comment: 'Irma issue app (api gateway)',
priceClass: PriceClass.PRICE_CLASS_100,
defaultBehavior: {
origin: new HttpOrigin(apiGatewayDomain),
originRequestPolicy: new OriginRequestPolicy(this, 'cf-originrequestpolicy', {
originRequestPolicyName: 'cfOriginRequestPolicyMijnUitkering',
headerBehavior: OriginRequestHeaderBehavior.allowList(
'Accept-Charset',
'Origin',
'Accept',
'Referer',
'Accept-Language',
'Accept-Datetime',
'Authoriz',
),
}),
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: AllowedMethods.ALLOW_ALL,
cachePolicy: new CachePolicy(this, 'cf-caching', {
cachePolicyName: 'cfCachingSessionsMijnUitkering',
cookieBehavior: CacheCookieBehavior.all(),
headerBehavior: CacheHeaderBehavior.allowList('Authorization'),
queryStringBehavior: CacheQueryStringBehavior.all(),
defaultTtl: Duration.seconds(0),
minTtl: Duration.seconds(0),
maxTtl: Duration.seconds(1),
}),
responseHeadersPolicy: this.responseHeadersPolicy(),
},
//logBucket: this.logBucket(),
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2019,
});
return distribution;
}

/**
* Add DNS records for cloudfront to the Route53 Zone
*
* Requests to the custom domain will correctly use cloudfront.
*
* @param distribution the cloudfront distribution
*/
addDnsRecords(distribution: Distribution) {
const zoneId = SSM.StringParameter.valueForStringParameter(this, Statics.hostedZoneId);
const zoneName = SSM.StringParameter.valueForStringParameter(this, Statics.hostedZoneName);
Expand All @@ -83,29 +130,11 @@ export class CloudFrontStack extends Stack {
});
}

/**
* bucket voor cloudfront logs
*/
// logBucket() {
// const cfLogBucket = new Bucket(this, 'CloudfrontLogs', {
// blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
// encryption: BucketEncryption.S3_MANAGED,
// lifecycleRules: [
// {
// id: 'delete objects after 180 days',
// enabled: true,
// expiration: Duration.days(180),
// },
// ],
// });
// return cfLogBucket;
// }


/**
* Get a set of (security) response headers to inject into the response
* @returns {ResponseHeadersPolicy} cloudfront responseHeadersPolicy
*/
* Get a set of (security) response headers to inject into the response
* @returns {ResponseHeadersPolicy} cloudfront responseHeadersPolicy
*/
responseHeadersPolicy(): ResponseHeadersPolicy {

const responseHeadersPolicy = new ResponseHeadersPolicy(this, 'headers', {
Expand All @@ -114,25 +143,87 @@ export class CloudFrontStack extends Stack {
contentTypeOptions: { override: true },
frameOptions: { frameOption: HeadersFrameOption.DENY, override: true },
referrerPolicy: { referrerPolicy: HeadersReferrerPolicy.NO_REFERRER, override: true },
strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true },
strictTransportSecurity: { accessControlMaxAge: Duration.days(366), includeSubdomains: true, override: true },
},
});
return responseHeadersPolicy;
}

/**
* Get the cleaned, trimmed header values for the csp header
*
* @returns string csp header values
*/
cspHeaderValue() {
const cspValues = 'default-src \'self\';\
frame-ancestors \'self\';\
frame-src \'self\';\
connect-src \'self\' https://componenten.nijmegen.nl;\
style-src \'self\' https://componenten.nijmegen.nl https://fonts.googleapis.com https://fonts.gstatic.com \
\'sha256-hS1LM/30PjUBJK3kBX9Vm9eOAhQNCiNhf/SCDnUqu14=\' \'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=\' \'sha256-OTeu7NEHDo6qutIWo0F2TmYrDhsKWCzrUgGoxxHGJ8o=\';\
script-src \'self\' https://componenten.nijmegen.nl https://siteimproveanalytics.com;\
font-src \'self\' https://componenten.nijmegen.nl https://fonts.gstatic.com;\
img-src \'self\' https://componenten.nijmegen.nl data: https://*.siteimproveanalytics.io;\
object-src \'self\';\
';
frame-ancestors \'self\';\
frame-src \'self\';\
connect-src \'self\' https://componenten.nijmegen.nl;\
style-src \'self\' https://componenten.nijmegen.nl https://fonts.googleapis.com https://fonts.gstatic.com \
\'sha256-hS1LM/30PjUBJK3kBX9Vm9eOAhQNCiNhf/SCDnUqu14=\' \'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=\' \'sha256-OTeu7NEHDo6qutIWo0F2TmYrDhsKWCzrUgGoxxHGJ8o=\';\
script-src \'self\' https://componenten.nijmegen.nl https://siteimproveanalytics.com;\
font-src \'self\' https://componenten.nijmegen.nl https://fonts.gstatic.com;\
img-src \'self\' https://componenten.nijmegen.nl data: https://*.siteimproveanalytics.io;\
object-src \'self\';\
';
return cspValues.replace(/[ ]+/g, ' ').trim();
}

}
/**
* Create an s3 bucket to hold static resources.
* Must be unencrypted to allow cloudfront to serve
* these resources.
*
* @returns S3.Bucket
*/
staticResourcesBucket() {
const bucket = new S3.Bucket(this, 'resources-bucket', {
blockPublicAccess: S3.BlockPublicAccess.BLOCK_ALL,
encryption: S3.BucketEncryption.UNENCRYPTED,
});

return bucket;
}

/**
* Allow listBucket to the origin access identity
*
* Necessary so cloudfront receives 404's as 404 instead of 403. This also allows
* a listing of the bucket if no /index.html is present in the bucket.
*
* @param originAccessIdentity
* @param bucket
*/
allowOriginAccessIdentityAccessToBucket(originAccessIdentity: OriginAccessIdentity, bucket: Bucket) {
bucket.addToResourcePolicy(new IAM.PolicyStatement({
resources: [
`${bucket.bucketArn}`,
`${bucket.bucketArn}/*`,
],
actions: [
's3:GetObject',
's3:ListBucket',
],
effect: IAM.Effect.ALLOW,
principals: [originAccessIdentity.grantPrincipal],
}),
);
}

/**
* Deploy contents of folder to the s3 bucket
*
* Invalidates the correct cloudfront path
* @param bucket s3.Bucket
* @param distribution Distribution
*/
deployBucket(bucket: S3.Bucket, distribution: Distribution) {
//Deploy static resources to s3
new aws_s3_deployment.BucketDeployment(this, 'staticResources', {
sources: [aws_s3_deployment.Source.asset('./src/app/static-resources/')],
destinationBucket: bucket,
distribution: distribution,
distributionPaths: ['/static/*'],
});
}
}

0 comments on commit 01c4702

Please sign in to comment.