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