概要
- フロントにNuxt.jsを採用していると、
npm run start
するだけだとX-Frame-Options
とかCSP
とかx-xss-protection
といったヘッダがレスポンスに付与されない -
Fastify
をNodeサーバーとして使っているのでそちらで頑張ってもよかったけど、せっかくAWSでCloudFront
->ALB
->Fargate
な仕組みを使っているので、AWSで頑張ってみようと思った - Lambda@Edgeを使って、Nuxtからのレスポンスにヘッダを付与するやつを組んだ
- アプリケーションと連動して動いたほうがいいし、手動でデプロイする運用がしんどいと思ったので、GitHub ActionsでCLIベースの自動デプロイが組めるところまで持ってきた
- それをやるがためにCloudFormationまで導入しててんやわんやした
日本語文献がそんなになかったので、まとまりがないメモですが貼っておきます。
Lambda@Edgeのソース内容
下記ブログを参考にしました。CSPだけ模倣するわけにいかなかったので、配列ベースでドメインを管理してjoinする感じで組む改修はしました。
http://blog.serverworks.co.jp/tech/2020/02/27/add-security-headers-lambda-edge-cloudfront/
'use strict';
exports.handler = (event, context, callback) => {
//Get contents of response
const cf = event.Records[0].cf
const request = cf.request
const response = cf.response;
const headers = response.headers;
//Set new headers
headers['strict-transport-security'] = [{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubdomains; preload'
}];
headers['content-security-policy'] = [{
key: 'Content-Security-Policy',
// 配列ベースで管理する
value: `default-src ${CSPDefaultSrcList}; img-src * blob: data:; script-src ${CSPScriptSrcList}; style-src ${CSPStyleSrcList}; object-src 'none';`
}];
headers['x-content-type-options'] = [{key: 'X-Content-Type-Options', value: 'nosniff'}];
// getFrameOptionsはrequestを受け取って、iframe用のページだけDENYしなかったりする
headers['x-frame-options'] = [{key: 'X-Frame-Options', value: getFrameOptions(request)}];
headers['x-xss-protection'] = [{key: 'X-XSS-Protection', value: '1; mode=block'}];
headers['referrer-policy'] = [{key: 'Referrer-Policy', value: 'same-origin'}];
//Return modified response
callback(null, response);
};
// 以下略
大まかな設計
-
雑に説明すると、Lambda@Edgeをデプロイするには、ソースコードを更新するだけじゃなくて、紐付けているCloudFrontも一緒に更新する必要がある
-
プロダクションコードと同じリポジトリに、awsディレクトリを切って、Lambda@Edgeのソースを配置
-
CloudFrontとLambdaを作成するCloudFormationのyamlも置いておく
-
Develop/MasterマージしたときにGitHub Actionsが動いて、前述のyamlを使ってAWS CLIでデプロイする
- GitHub ActionsでAWS CLI動かすのは公式がアクションを出してくれているので簡単
- name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1
組み方
- すでにCloudFrontは事前にコンソールで作っていたので、CloudFormerを使ってCloudFormationのymlにエクスポートした
- CloudFormerは癖が強いけど、公式ドキュメントのやり方通りにできる
CloudFront/Lambda@EdgeのCloudFormation
自社の構成がバレるので一部だけ引用して貼る。同じyml内にLambdaとCloudFront両方書く
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
AwsLambdaRole:
Type: String
Description: Names of existing Roles you want to add to the newly created Managed Policy
# ...中略...
Resources:
AddSecurityHeaderToResponse:
Type: AWS::Serverless::Function
Properties:
CodeUri: AddSecurityHeaderToResponse/
Role: !Ref AwsLambdaRole
Runtime: nodejs12.x
Handler: index.handler
Timeout: 5
AutoPublishAlias: stg
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "sts:AssumeRole"
Principal:
Service:
- "lambda.amazonaws.com"
- "edgelambda.amazonaws.com"
# ...中略...
StagingCloudFront:
Type: 'AWS::CloudFront::Distribution'
Properties:
# ...中略...
DefaultCacheBehavior:
LambdaFunctionAssociations:
- EventType: origin-response
LambdaFunctionARN: !Ref AddSecurityHeaderToResponse.Version
GitHub Actions
こんな感じでAWSのCLIコマンドを実行すればいい。環境変数は適宜。if
を使えるので環境変数を切り替えることで検証環境と本番環境で1ファイルで運用することができます。
- name: Deploy latest Lambda function
run: |
aws cloudformation package --template-file $AWS_STACK_FILE_NAME --output-template-file packaged.yaml --s3-bucket $BUCKET_NAME --region us-east-1
working-directory: ./aws/lambda_edge
- name: Update Cloudfront Distribution
run: |
aws cloudformation deploy --template-file packaged.yaml --stack-name $AWS_STACK_NAME --parameter-overrides MainStreamBackendAcm=$AWS_CERTIFICATE_ARN AwsLambdaRole=${{ secrets.AWS_LAMBDA_EDGE_ROLE }} --region us-east-1
working-directory: ./aws/lambda_edge
メリデメ
- Pros
- 一旦CSP頑張ってみることにしたので、アプリケーションでドメイン追加したときに必ず(ほぼ)同期デプロイで出せる
- Lambdaを使うとローカル開発ではヘッダーを付与するのができないので、せめて同一リポジトリにおいて意識できる
- Nginxの設定ファイルの管理のほうが個人的に辛いのでそれしなくてよくなったのがよい
- Nuxt以外のサービスを導入することになったとしても、CloudFrontのレイヤーでHTTPヘッダを担保しているとセキュリティ安心感ある
- Cons
- 普通にCloudFormationつらかった
- IAM権限で必要なのが多すぎて、リソースや権限を*使わないように頑張るのがつらかった
- 別にインフラ全部をCloudFormationしたいわけじゃなかったのに、Lambda@Edgeをリリースする関係で使い始めてしまったので中途半端
CloudFormationとか全く知らなかったので結構手間取りました。
1日半くらい溶かしてしまったので、まあまあ大変な方でしたね。
参考文献
CloudFormerで現状のCloudFrontの値をとりあえず取得
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-using-cloudformer.html
めっちゃβ版って感じのUXでした
CloudFormationでLambdaのデプロイ
https://qiita.com/ytaka95/items/5899c44c85e71fdc5273
CloudFormationでLambda@Edgeをデプロイする - Qiita
CLIでCloudFormationをデプロイする公式Doc
基本はpackageを動かしたあとにdeployコマンド。
作成も更新も同じコマンドでいい。create-stack/update-stackとかあるけどdeployだけでオールOKっぽい ref: https://stackoverflow.com/questions/49945531/aws-cloudformation-create-stack-vs-deploy
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deploy/index.html
CloudFormationのyamlには外から引数を渡すことができる
開発と本番で違うところがあるからこれで引数で渡せるようにして、GitHub Actionsのyamlで環境変数を使って使い分ける感じ
CloudFormationでCloudFrontをデプロイする
公式が出している、ちょっと古いけどサンプルになるやつ
CloudFormation deployコマンドを叩いたときに無言で落ちるけど実際は権限エラーだよってやつ
https://github.com/awslabs/serverless-application-model/issues/58
あまり役に立たなかったけど足しにはなった公式のFAQ
https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-template-validation/
これも役に立たなかったけど一応見た
デプロイ時には専用のIAMユーザー作って権限を付ける
コレが一番大変だった。疲れた。
デプロイ用のIAM作って、手元でコマンドを叩いて権限不足で弾かれるのを延々と繰り返す。
AWSの設定している時、いつも「最小限のポリシーにしてから、失敗したら必要な権限だけ足していく」をやっているんだけど、これ無限に時間溶かさないですか?玄人の方々はどうやって乗り切ってるんだ
— 名人 | ㈱NoSchool CTO (@Meijin_garden) July 7, 2020
https://gist.github.com/stu-smith/7fec25367e1b83fb0709c708a704ff04
以上です。
よかったらTwitterフォローしてください〜
https://twitter.com/Meijin_garden