はじめに
最近、SSTを使ったサーバーレスアプリケーションの構築にトライしました。
そして以前、CDKとSAMを使ったアプリケーションに対して以下の記事を書いたのですが、
SST版でも同様の記事を書いておきたかったので、本記事の作成に至った形となります。
概要
API Gateway V2(HTTP API)のエンドポイントへのアクセスをCloudFront経由のみに制限したいが、HTTP APIはWAFのサポートがされていないため、Lambda Authorizerで制御します。そのための環境構築を、SSTを用いて行うといった内容になります。
SSTでサーバーレス環境を構築してみたらすごく良かったで登場したコードのカスタマイズを行う形になるため、記事を読んだ後の方が本記事の理解が進みやすいと思います。
※SSTのコードを中心とした簡潔な説明にしたいため、以前のセキュリティ記事では説明したシークレットのローテーション部分は省略します。
内容
以下順番で説明していきます。
- バックエンドへのリクエストをCloudFront経由にする
- API Gateway v2(HTTP API)側でLambda Authorizerを起動する
1. バックエンドへのリクエストをCloudFront経由にする
SSTでサーバーレス環境を構築してみたらすごく良かったの記事では、以下コードがCloudFrontの設定に相当する部分でした。
new sst.aws.StaticSite("MyWeb", {
domain: {
// Route53のホストゾーンを参照する
// Route53にAレコードやCNAMEの追加をしてくれる
// 自動でus-east-1でSSL証明書を取得し、CloudFrontに証明書を紐づけてくれる
name: "ドメイン名"
},
// 全てのパスのCloudFrontキャッシュが削除されるまで待機する設定
// https://sst.dev/docs/component/aws/static-site#invalidation
invalidation: {
wait: true
},
build: {
command: "npm run generate",
output: ".output/public"
},
});
よってこのコードを修正し、API Gateway を対象としたオリジンの追加・ビヘイビアの設定を行なっていきます。
具体的な設定方法に関してはサンプルコードがありますので、これを参考にしながら修正していきます。
まず、オリジンやビヘイビアをカスタマイズしようとすると、transformの設定が必要となります。transformにより、用意されたcomponentの構造を変換する形となります。
transform
transformに関して、ドキュメントの読み方を理解するのに手間取ったので、以下にドキュメントの読み方を記載します。
-
ドキュメント内のtransform.cdn?のType CdnArgsをクリックし、originsとorderedCacheBehaviors?を確認します。
-
originsでは、DistributionOriginを確認することでPulumi側のサイトに飛び、設定可能なパラメータを諸々確認できます。
-
同様にorderedCacheBehaviors?では、DistributionOrderedCacheBehaviorを確認することで、Pulumi側のサイトに飛び、設定可能なパラメータを諸々確認できます。
最終的な完成版はこちらです。
// API Gateway Settings
const api = new sst.aws.ApiGatewayV2("MyApi", {
cors: {
allowOrigins: [
// cors許可するドメイン名
"ドメイン名"
]
}
});
// 省略
// CloudFront Settings
// オリジン設定
const origin = {
domainName: $resolve(api.url).apply(url => new URL(url).host), // https://の除去を行い、ドメインのみ抽出する
originId: "apiOrigin", // オリジン名になる
customOriginConfig: {
httpPort: 80,
httpsPort: 443,
originSslProtocols: ["TLSv1.2"],
originProtocolPolicy: "https-only", // HTTPSのみに制限する
},
customHeaders: [{
name: "x-origin-verify",
value: "トークン値",
}],
};
// ビヘイビア設定
const cacheBehavior = {
pathPattern: "/api/*", // /api/* パスのリクエストを対象とする
targetOriginId: origin.originId, // 上記originをターゲットに設定する
viewerProtocolPolicy: "redirect-to-https", // HTTPのリクエストはHTTPSにリダイレクトする
allowedMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], // リクエスト許可するHTTPメソッド
cachedMethods: ["GET", "HEAD"], // キャッシュ対象にするHTTPメソッド
cachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", // CachingDisabled
originRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac", // AllViewerExceptHostHeader
};
// Static Site Settings
new sst.aws.StaticSite("MyWeb", {
domain: {
name: "ドメイン名"
},
invalidation: {
wait: true
},
// Nuxt 3の静的build用。Viteではこのオプションは不要
build: {
command: "npm run generate",
output: ".output/public"
},
// transformでcomponentの構造を変換する
transform: {
cdn: (options: sst.aws.CdnArgs) => {
options.origins = $resolve(options.origins).apply(val => [...val, origin]);
options.orderedCacheBehaviors = $resolve(
options.orderedCacheBehaviors || []
).apply(val => [...val, cacheBehavior]);
},
},
});
1点補足で、こちらのidに関しては、AWSのドキュメントに記載がありますのでそこから値を引っ張ってきました。
CachingDisabledとAllViewerExceptHostHeaderはAPI Gatewayを使う場合に推奨される設定となっているため、準拠した形となります。
cachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", // CachingDisabled
originRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac", // AllViewerExceptHostHeader
2. API Gateway v2(HTTP API)でLambda Authorizerを起動する
まずは、認証に使うLambda関数に対して、認証ロジックを記載します。
import { APIGatewayEvent } from 'aws-lambda';
export const handler = async (
event: APIGatewayEvent,
): Promise<{ isAuthorized: boolean; context?: any }> => {
const originVerify = event.headers?.['x-origin-verify'];
try {
// リクエストのヘッダーに含まれるトークンが一致しているかチェックする
if (originVerify === "トークン値") {
return {
isAuthorized: true,
context: {
user: 'user',
},
};
}
} catch (error) {
console.error('Error retrieving secret:', error);
}
return {
isAuthorized: false,
};
};
実際はトークンの値はSecretsに保存してそれを参照する形としていますが、今回は説明を簡易的にするため、"トークン値" という形で記載しています。
次に、addAuthorizer のメソッドを使い、API Gatewayに対してLambda Authorizerを設定します。
// Lambda Authorizer
const lambdaAuthorizer = api.addAuthorizer({
name: "lambdaAuthorizer",
lambda: {
function: {
handler: "src/authorizer.handler",
},
identitySources: ["$request.header.x-origin-verify"], // リクエストヘッダーのどの値を認証に使うか指定する
},
});
最後に、API Gatewayの各RouteにLambda Authorizerを紐づけます。
// API Route
api.route("GET /api/get",
{
handler: "src/get.handler",
},
{
auth: {
lambda: lambdaAuthorizer.id,
},
}
);
これで設定は完了となります!
さいごに
前回の【セキュリティ】Lambda AuthorizerでHTTP API GatewayへのリクエストをCloudFrontだけに制限するの記事と比較して内容を省略した部分があるとはいえ、SSTの構成ファイルの方がより見やすく簡潔な記述で済んだと感じます。
ただ、transformのプロパティを弄る場合の癖は少し強いかなと思いますので、初めてキャッチアップする際は少し大変かもしれません。