Webのセキュリティヘッダーを設定する

# 目的
Webを作成した時にセキュリティヘッダーを設定したいです。
ここではAWSのCloudFrontを想定して、CloudFront Functionのコードとそれを配置するCDKを書きます。
またCSPで問題が起きた場合にレポートするサイトをAWS Lambdaで作成しました。

# コード

## レポート

```ruby:report/main.rb
require 'json'
require 'logger'

def lambda_handler(event:, context:)
  logger = Logger.new($stdout)
  body = JSON.parse(event['body'])
  logger.info(body)
  {
    statusCode: 200,
    body: {}.to_json
  }
end
```

```typescript:cdk-report-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cdk from 'aws-cdk-lib';
import { getProcejctName } from './util';

export class CdkReportStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const procejctName = 'my-project'

    // CSPレポート
    const reportFunction = new lambda.Function(this, 'CspReportHandler', {
      runtime: lambda.Runtime.RUBY_3_2,
      code: lambda.Code.fromAsset('report'),
      handler: 'main.lambda_handler',
      timeout: cdk.Duration.seconds(60),
      functionName: `${procejctName}-report`,
    });
    const api = new apigateway.RestApi(this, 'ReportAPI', {
      restApiName: `${procejctName}-report`,
      deployOptions: {
        stageName: 'production'
      }
    })
    const report = api.root.addMethod('POST', new apigateway.LambdaIntegration(
      reportFunction
    ))
  }
}
```

## CloudFront Functions


```js:security_header.js
function handler(event) {
    var response = event.response;
    var headers = response.headers;

    // HTTPSのみ許可(2年間、サブドメインを含む、HSTS)
    // https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Strict-Transport-Security
    headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; 

    // CSP
    // https://developer.mozilla.org/ja/docs/Web/HTTP/CSP
    // https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy/report-to

    // レポートを受け付けるエンドポイントを指定
    var endpoint = 'https://report.example.com'
    headers['reporting-endpoints'] = { value: `endpoint="${endpoint}"` }

    // ベースとなるサイトを設定
    var base = "'self' my.example.com your.example.com"

    // 良く使うGoogleのサイト
    var google = "https://www.googletagmanager.com https://www.google-analytics.com"

    // 良く使うアマゾンのサイト
    var amazon = "*.amazonaws.com"

    var csps = [
        `connect-src  ${base} ${google} ${amazon}`,
        `default-src ${base}`,
        `font-src 'self' ${base} ${amazon}`,
        `frame-src ${base}`,
        `img-src ${base} ${amazon}`,
        `media-src ${base} ${amazon}`,
        `object-src ${base}`,
        `script-src ${base} ${google}`,
        `style-src ${base}`,
        `worker-src ${base}`,
        'report-to endpoint',
        `report-uri ${endpoint}`,
    ]
    headers['content-security-policy'] = { value: csps.join('; ')}; 

    // MIMEスニッフィングを禁止、コンテンツの内容でContentTypeを無視させない
    // https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Content-Type-Options
    headers['x-content-type-options'] = { value: 'nosniff'}; 

    // 他のサイトのiFrameに埋め込まさせない
    // https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Frame-Options
    headers['x-frame-options'] = {value: 'DENY'}; 

    return response;
}
```

```typescript:cdk-cloud-front-function-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';

export class CdkCloudFrontFunctionStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const procejctName = 'my-project'

    // CloudFront Functions
    const addHeaderFunction = new cloudfront.Function(
      this,
      "AddHeaderFunction",
      {
        functionName: `${procejctName}-security-headers`,
        code: cloudfront.FunctionCode.fromFile({
          filePath: "security_headers.js",
        }),
      }
    );
    this.exportValue(addHeaderFunction.functionArn, {name: `${procejctName}-security-headers-arn`})
  }
}
```