diff --git a/.yarn/cache/serverless-lambda-edge-pre-existing-cloudfront-npm-1.2.0-23ac40bc09-267d921afe.zip b/.yarn/cache/serverless-lambda-edge-pre-existing-cloudfront-npm-1.2.0-23ac40bc09-267d921afe.zip deleted file mode 100644 index 9e5c777b38..0000000000 Binary files a/.yarn/cache/serverless-lambda-edge-pre-existing-cloudfront-npm-1.2.0-23ac40bc09-267d921afe.zip and /dev/null differ diff --git a/packages/security-header-lambda/.eslintrc.js b/packages/security-header-lambda/.eslintrc.js deleted file mode 100644 index e3479324ae..0000000000 --- a/packages/security-header-lambda/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -const { baseEslint } = require('../ts-scripts') - -module.exports = baseEslint diff --git a/packages/security-header-lambda/README.md b/packages/security-header-lambda/README.md deleted file mode 100644 index 2ce3dab942..0000000000 --- a/packages/security-header-lambda/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# API Docs Lambda - -Super simple proxy to [https://foundations-documentation.reapit.cloud] to allow [https://marketplace.reapit.cloud] to load the documentation in an iframe by setting a 'Content-Security-Policy' header. diff --git a/packages/security-header-lambda/package.json b/packages/security-header-lambda/package.json deleted file mode 100644 index 60a3e1dfa3..0000000000 --- a/packages/security-header-lambda/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@reapit/security-header-lambda", - "version": "1.0.9", - "homepage": "https://github.com/reapit/foundations", - "license": "MIT", - "author": "reapit global", - "bugs": { - "url": "https://github.com/reapit/foundations/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/reapit/foundations.git" - }, - "devDependencies": { - "@reapit/config-manager": "workspace:packages/config-manager", - "@reapit/ts-scripts": "workspace:packages/ts-scripts", - "@typescript-eslint/eslint-plugin": "^8.10.0", - "@typescript-eslint/parser": "^8.10.0", - "eslint": "8.57.1", - "eslint-plugin-prettier": "^5.2.1", - "serverless": "^3.39.0", - "serverless-deployment-bucket": "^1.6.0", - "serverless-lambda-edge-pre-existing-cloudfront": "^1.2.0", - "serverless-plugin-log-retention": "^2.0.0", - "snyk": "^1.1293.1", - "tsup": "^6.7.0", - "typescript": "^5.6.3" - }, - "scripts": { - "start": "echo '...skipping...'", - "test": "echo '...skipping...'", - "build": "tsup && cd dist && zip -r index.zip index.js", - "lint": "eslint --cache --ext=ts,tsx,js src", - "check": "tsc --noEmit --skipLibCheck", - "release": "serverless deploy", - "deploy": "echo '...skipping...'", - "publish": "echo '...skipping...'", - "conf": "yarn config-manager --namespace cloud --entity security-header-lambda --name local --mode fetch", - "commit": "yarn lint" - }, - "dependencies": { - "aws-lambda": "^1.0.7" - } -} diff --git a/packages/security-header-lambda/serverless.yml b/packages/security-header-lambda/serverless.yml deleted file mode 100644 index 8ed3bb95b9..0000000000 --- a/packages/security-header-lambda/serverless.yml +++ /dev/null @@ -1,110 +0,0 @@ -service: cloud-security-header-lambda -plugins: - - serverless-offline - - serverless-deployment-bucket - - serverless-lambda-edge-pre-existing-cloudfront - - serverless-plugin-log-retention - -custom: - env: ${file(./config.json)} - logRetentionInDays: 30 - -provider: - name: aws - runtime: ${opt:runtime, 'nodejs18.x' } - stage: ${opt:stage, 'dev'} - region: 'us-east-1' - lambdaHashingVersion: '20201221' - deploymentBucket: - name: cloud-security-header-lambda-${opt:stage, 'dev'} - blockPublicAccess: false - -resources: - Resources: - IamRoleLambdaExecution: - Type: 'AWS::IAM::Role' - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - - edgelambda.amazonaws.com - Action: sts:AssumeRole - -package: - individually: true - artifact: './dist/index.zip' - -functions: - securityHeaderLambda: - handler: src/index.securityHeaderLambda - events: - - preExistingCloudFront: - distributionId: ${self:custom.env.developerPortalCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.marketplaceCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.geoDiaryCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.marketplaceManagementCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.developerAdminCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.dataWarehouseCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.paymentsCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.reapitConnectCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.paymentsPortalCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.mfaConfigCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} - - preExistingCloudFront: - distributionId: ${self:custom.env.marketplaceAdminCfDistId} - eventType: origin-response - pathPattern: '*' - includeBody: false - stage: ${opt:stage, 'dev'} diff --git a/packages/security-header-lambda/src/index.ts b/packages/security-header-lambda/src/index.ts deleted file mode 100644 index 00158449fb..0000000000 --- a/packages/security-header-lambda/src/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CloudFrontResponseEvent, Context, CloudFrontResponseCallback } from 'aws-lambda' -import config from '../config.json' - -const { - defaultContentSecurityPolicy, - imageContentSecurityPolicy, - scriptContentSecurityPolicy, - styleContentSecurityPolicy, - objectContentSecurityPolicy, - fontContentSecurityPolicy, - frameContentSecurityPolicy, -} = config - -export const securityHeaderLambda = ( - event: CloudFrontResponseEvent, - context: Context, - callback: CloudFrontResponseCallback, -) => { - const response = event.Records[0].cf.response - const headers = response.headers - const paymentsClients = [config.paymentsCfDistId, config.paymentsPortalCfDistId] - const cfDistId = event.Records[0].cf.config.distributionId - const isPayments = paymentsClients.includes(cfDistId) - // Support for cross origin iframes to allow for 3D Secure where we don't know the orginating bank - const iframePolicy = isPayments ? "frame-src 'self' https://*" : frameContentSecurityPolicy - - headers['strict-transport-security'] = [ - { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' }, - ] - headers['content-security-policy'] = [ - { - key: 'Content-Security-Policy', - value: `${defaultContentSecurityPolicy}; ${imageContentSecurityPolicy}; ${scriptContentSecurityPolicy}; ${styleContentSecurityPolicy}; ${objectContentSecurityPolicy}; ${fontContentSecurityPolicy}; ${iframePolicy}`, - }, - ] - headers['x-content-type-options'] = [{ key: 'X-Content-Type-Options', value: 'nosniff' }] - headers['x-frame-options'] = [{ key: 'X-Frame-Options', value: 'DENY' }] - headers['x-xss-protection'] = [{ key: 'X-XSS-Protection', value: '1; mode=block' }] - headers['referrer-policy'] = [{ key: 'Referrer-Policy', value: 'same-origin' }] - - callback(null, response) -} - -export default securityHeaderLambda diff --git a/packages/security-header-lambda/tsconfig.json b/packages/security-header-lambda/tsconfig.json deleted file mode 100644 index 7f5e12082a..0000000000 --- a/packages/security-header-lambda/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "module": "commonjs", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "target": "es6", - "noImplicitAny": false, - "skipLibCheck": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "sourceMap": true, - "typeRoots": ["./node_modules/@types", "../../node_modules/@types"] - }, - "include": ["./src/**/*"], - "exclude": ["./dist", "./node_modules"] -} diff --git a/packages/security-header-lambda/tsup.config.js b/packages/security-header-lambda/tsup.config.js deleted file mode 100644 index 3a6794797c..0000000000 --- a/packages/security-header-lambda/tsup.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'tsup' -import fs from 'fs' -import config from './config.json' - -const pkgJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')) - -export default defineConfig({ - entry: ['src/index.ts'], - target: 'node18', - clean: true, - minify: config.NODE_ENV === 'production', - esbuildOptions: (opts) => { - opts.resolveExtensions = ['.ts', '.mjs', '.js'] - }, - noExternal: Object.keys(pkgJson.dependencies), - external: [], -}) diff --git a/packages/ts-scripts/src/cdk/components/security-headers.ts b/packages/ts-scripts/src/cdk/components/security-headers.ts new file mode 100644 index 0000000000..632c61cb50 --- /dev/null +++ b/packages/ts-scripts/src/cdk/components/security-headers.ts @@ -0,0 +1,55 @@ +import { Duration, Stack } from 'aws-cdk-lib' +import { + CfnDistribution, + CloudFrontWebDistribution, + Distribution, + HeadersFrameOption, + HeadersReferrerPolicy, + ResponseHeadersPolicy, +} from 'aws-cdk-lib/aws-cloudfront' + +const defaultContentSecurityPolicy = + "default-src 'self' *.reapit.cloud *.chatlio.com *.pusher.com *.googleapis.com https://cognito-idp.eu-west-2.amazonaws.com https://cognito-idp.ap-southeast-2.amazonaws.com https://www.google-analytics.com *.sentry.io https://sentry.io *.visualstudio.com *.sagepay.com *.elavon.com https://reapit-marketplace-app-media-dev.s3.eu-west-2.amazonaws.com https://reapit-swagger-dev.s3.eu-west-2.amazonaws.com https://reapit-swagger-prod.s3.eu-west-2.amazonaws.com https://reapit-marketplace-app-media-prod.s3.eu-west-2.amazonaws.com blob: *.googleapis.com *.pusherapp.com wss://ws.pusherapp.com *.zdassets.com *.zendesk.com wss://widget-mediator.zopim.com wss://ws-eu.pusher.com *.mixpanel.com https://api.github.com/ *.chatbase.co" +const imageContentSecurityPolicy = + "img-src 'self' data: *.reapit.cloud *.chatlio.com https://cdn.jsdelivr.net https://maps.gstatic.com https://maps.googleapis.com blob: https://reapit-marketplace-app-media-dev.s3.eu-west-2.amazonaws.com https://reapit-marketplace-app-media-prod.s3.eu-west-2.amazonaws.com https://*.githubusercontent.com *.chatbase.co" +const scriptContentSecurityPolicy = + "script-src 'self' *.reapit.cloud *.chatlio.com https://cdn.jsdelivr.net https://unpkg.com *.sagepay.com *.elavon.com https://maps.googleapis.com https://www.google-analytics.com *.zdassets.com *.pusher.com *.zopim.com *.chatbase.co" +const styleContentSecurityPolicy = + "style-src 'self' 'unsafe-inline' *.reapit.cloud *.chatlio.com https://cdn.jsdelivr.net https://fonts.googleapis.com" +const objectContentSecurityPolicy = "object-src 'self' *.reapit.cloud" +const fontContentSecurityPolicy = "font-src 'self' *.reapit.cloud https://fonts.gstatic.com" +const frameContentSecurityPolicy = + "frame-src 'self' *.reapit.cloud https://foundations-documentation.reapit.cloud *.powerbi.com *.powerapps.com *.windows.net *.visualstudio.com https://www.youtube.com *.reapit.com https://player.vimeo.com https://explorer.embed.apollographql.com *.chatbase.co" + +export const createSecurityHeaders = (stack: Stack, id: string, webDistribution: CloudFrontWebDistribution) => { + const iframePolicy = stack.stackName.includes('payments') ? "frame-src 'self' https://*" : frameContentSecurityPolicy + const contentSecurityPolicy = `${defaultContentSecurityPolicy}; ${imageContentSecurityPolicy}; ${scriptContentSecurityPolicy}; ${styleContentSecurityPolicy}; ${objectContentSecurityPolicy}; ${fontContentSecurityPolicy}; ${iframePolicy}` + const myResponseHeadersPolicy = new ResponseHeadersPolicy(stack, id, { + securityHeadersBehavior: { + contentSecurityPolicy: { contentSecurityPolicy, override: true }, + contentTypeOptions: { override: true }, + frameOptions: { frameOption: HeadersFrameOption.DENY, override: true }, + referrerPolicy: { referrerPolicy: HeadersReferrerPolicy.NO_REFERRER, override: true }, + strictTransportSecurity: { + accessControlMaxAge: Duration.seconds(63072000), + preload: true, + includeSubdomains: true, + override: true, + }, + xssProtection: { + protection: true, + modeBlock: true, + override: true, + }, + }, + }) + + const cfnDistribution = webDistribution.node.defaultChild as CfnDistribution | undefined + if (!cfnDistribution) { + throw new Error('Could not find CloudFront Distribution') + } + cfnDistribution.addPropertyOverride( + 'DistributionConfig.DefaultCacheBehavior.ResponseHeadersPolicyId', + myResponseHeadersPolicy.responseHeadersPolicyId, + ) +} diff --git a/packages/ts-scripts/src/cdk/components/site.ts b/packages/ts-scripts/src/cdk/components/site.ts index 3f06c18de1..4de2470325 100644 --- a/packages/ts-scripts/src/cdk/components/site.ts +++ b/packages/ts-scripts/src/cdk/components/site.ts @@ -14,6 +14,7 @@ import { CanonicalUserPrincipal, PolicyStatement } from 'aws-cdk-lib/aws-iam' import { BlockPublicAccess, BucketAccessControl, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3' import { ACM } from 'aws-sdk' import { InvalidateCloudfrontDistribution } from '../utils/cf-innvalidate' +import { createSecurityHeaders } from './security-headers' export interface CreateSiteInterface { env?: 'dev' | 'prod' @@ -131,6 +132,8 @@ export const createSite = async ( ], }) + createSecurityHeaders(stack, 'SecurityHeaders', distribution) + createRoute(stack, 'route', { target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)), zone: hostedZone, diff --git a/yarn.lock b/yarn.lock index dac98126e5..c37e43d409 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8051,27 +8051,6 @@ __metadata: languageName: unknown linkType: soft -"@reapit/security-header-lambda@workspace:packages/security-header-lambda": - version: 0.0.0-use.local - resolution: "@reapit/security-header-lambda@workspace:packages/security-header-lambda" - dependencies: - "@reapit/config-manager": "workspace:packages/config-manager" - "@reapit/ts-scripts": "workspace:packages/ts-scripts" - "@typescript-eslint/eslint-plugin": "npm:^8.10.0" - "@typescript-eslint/parser": "npm:^8.10.0" - aws-lambda: "npm:^1.0.7" - eslint: "npm:8.57.1" - eslint-plugin-prettier: "npm:^5.2.1" - serverless: "npm:^3.39.0" - serverless-deployment-bucket: "npm:^1.6.0" - serverless-lambda-edge-pre-existing-cloudfront: "npm:^1.2.0" - serverless-plugin-log-retention: "npm:^2.0.0" - snyk: "npm:^1.1293.1" - tsup: "npm:^6.7.0" - typescript: "npm:^5.6.3" - languageName: unknown - linkType: soft - "@reapit/ts-bundler@workspace:packages/ts-bundler": version: 0.0.0-use.local resolution: "@reapit/ts-bundler@workspace:packages/ts-bundler" @@ -26945,13 +26924,6 @@ __metadata: languageName: node linkType: hard -"serverless-lambda-edge-pre-existing-cloudfront@npm:^1.2.0": - version: 1.2.0 - resolution: "serverless-lambda-edge-pre-existing-cloudfront@npm:1.2.0" - checksum: 267d921afe59b5ae30045d5eaf0911129e5aa09dfd65762824fa95e57336d2e0490277789f0b2d3048967d3b97888e380bc7e550068e49f68d2498b861348f15 - languageName: node - linkType: hard - "serverless-offline@npm:^12.0.4": version: 12.0.4 resolution: "serverless-offline@npm:12.0.4"