CloudFront のオリジンカスタムヘッダーで Lambda 関数 URL への直接アクセスを制御してみた

CloudFront のオリジンカスタムヘッダーで Lambda 関数 URL への直接アクセスを制御してみた

CloudFront のオリジンカスタムヘッダーを使って、Lambda 関数 URL へのアクセス制御を検証しました。CloudFront 経由のリクエストは許可し、検証用ヘッダーを持たない直接アクセスは拒否する仕組みを実装・確認した結果をご紹介します。
2026.08.03

はじめに

テクニカルサポートの 片方 です。
オリジン側でその値を検証すると、検証用ヘッダーが一致しない Lambda 関数 URL へのリクエストを拒否できます。

本ブログでは、Lambda 関数 URL を CloudFront のオリジンに設定し、X-Origin-Verify ヘッダーを利用したアクセス制御を検証します。以下の結果を確認します。

  • CloudFront 経由のアクセス: 200 OK
  • Lambda 関数 URL への直接アクセス: 403 Forbidden

この構成では、Lambda 関数 URL を知っているだけのリクエストを拒否し、CloudFront が付与する検証用ヘッダーを持つリクエストだけを Lambda 関数で受け付けられます。
ただし、この方式は固定値を使ったリクエスト検証です。Lambda 関数 URL を非公開にするものではありません。関数 URL と検証用ヘッダーの値が漏えいした場合、第三者が CloudFront を経由せずに Lambda 関数 URL を呼び出せます。

本番環境で CloudFront 以外からのアクセスをより厳格に制御したい場合は、Origin Access Control(OAC)の利用を検討してください。Lambda 関数 URL で OAC を使用する場合は、認証タイプに AWS_IAM を指定し、CloudFront ディストリビューションからの呼び出しだけを許可するリソースベースポリシーを設定します。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html?utm_source=openai

検証したこと

CloudFront のオリジンカスタムヘッダーを利用して、CloudFront から Lambda 関数 URL へ送信されるリクエストに、次の固定ヘッダーを追加します。

X-Origin-Verify: <ランダムに生成した値>

Lambda 関数では、受信した X-Origin-Verify ヘッダーと環境変数に設定した値を比較します。
値が一致した場合は 200 OK、一致しない場合は 403 Forbidden を返します。検証内容は以下のとおりです。

アクセス先 クライアントが送信するヘッダー 期待結果
CloudFront なし 200 OK
Lambda 関数 URL なし 403 Forbidden
Lambda 関数 URL X-Origin-Verify: invalid 403 Forbidden
CloudFront X-Origin-Verify: invalid 200 OK

最後のパターンでは、クライアントが X-Origin-Verify: invalid を送信しても、CloudFront がオリジンカスタムヘッダーの設定値で上書きしてオリジンへ転送することを確認します。
CloudFront は、同名のヘッダーがビューワーリクエストに含まれている場合、オリジンカスタムヘッダーに設定した値で上書きします。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html?utm_source=openai

前提条件

本ブログでは、AWS マネジメントコンソールを使用してリソースを作成します。検証条件は以下のとおりです。

項目 設定
AWS リージョン アジアパシフィック(東京) ap-northeast-1
Lambda ランタイム Node.js 22.x
Lambda アーキテクチャ arm64
Lambda 関数 URL の認証タイプ NONE
CloudFront のキャッシュ 無効
独自ドメイン 使用しない
AWS WAF 使用しない
動作確認 AWS CloudShell の curl コマンド

CloudFront はグローバルサービスですが、オリジンとなる Lambda 関数は東京リージョンに作成します。
認証タイプに NONE を指定した Lambda 関数 URL では、IAM 認証は行われません。ただし、呼び出しにはリソースベースポリシーによる許可が必要です。
AWS マネジメントコンソールで認証タイプ NONE の関数 URL を作成した場合、Lambda は公開アクセスを許可するリソースベースポリシーを自動作成します。
そのため、関数 URL と検証用ヘッダーの値の両方を知る第三者は、CloudFront を経由せずにリクエストできる点に注意してください。
本ブログでは、Lambda 関数内でカスタムヘッダーを検証し、値が一致しないリクエストに 403 Forbidden を返します。
作成する主なリソースは以下のとおりです。

  • Lambda 関数
  • Lambda 関数 URL
  • Lambda 実行ロール
  • CloudFront ディストリビューション
  • Lambda 関数を初回実行した後、CloudWatch Logs のロググループも自動作成されます。

検証用ヘッダー値の生成と動作確認には、AWS マネジメントコンソールから起動できる AWS CloudShell を使用します。

やってみた

まず、CloudFront と Lambda 関数で共有する検証用ヘッダー値を生成します。
AWS CloudShell で次のコマンドを実行しました。

openssl rand -hex 32

出力された値は、以降の手順で CloudFront のオリジンカスタムヘッダーと Lambda 関数の環境変数に設定します。

0001

Lambda 関数の作成

AWS マネジメントコンソールで Lambda 関数を作成します。

項目 設定値
関数名 cloudfront-origin-header-sample
ランタイム Node.js 22.x
アーキテクチャ arm64
実行ロール 基本的な Lambda アクセス権限で新しいロールを作成

関数の作成後、コードを以下に置き換えます。

import { timingSafeEqual } from "node:crypto";

const HEADER_NAME = "x-origin-verify";

function secureCompare(actual, expected) {
  if (typeof actual !== "string" || typeof expected !== "string") {
    return false;
  }

  const actualBuffer = Buffer.from(actual, "utf8");
  const expectedBuffer = Buffer.from(expected, "utf8");

  if (actualBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return timingSafeEqual(actualBuffer, expectedBuffer);
}

function jsonResponse(statusCode, body) {
  return {
    statusCode,
    headers: {
      "content-type": "application/json; charset=utf-8",
      "cache-control": "no-store",
    },
    body: JSON.stringify(body),
  };
}

export const handler = async (event) => {
  const expectedSecret = process.env.ORIGIN_VERIFY_SECRET;

  if (!expectedSecret) {
    // 設定ミスの詳細やシークレットはレスポンスに含めない
    return jsonResponse(500, {
      message: "Internal Server Error",
    });
  }

  const headers = Object.fromEntries(
    Object.entries(event.headers ?? {}).map(([key, value]) => [
      key.toLowerCase(),
      value,
    ]),
  );

  const providedSecret = headers[HEADER_NAME];

  if (!secureCompare(providedSecret, expectedSecret)) {
    return jsonResponse(403, {
      message: "Forbidden",
    });
  }

  return jsonResponse(200, {
    message: "Accessed through CloudFront",
    path: event.rawPath ?? "/",
    method: event.requestContext?.http?.method ?? "UNKNOWN",
  });
};

このコードでは、以下を実施しています。

  • 環境変数 ORIGIN_VERIFY_SECRET から期待値を取得する
  • リクエストヘッダーから x-origin-verify を取得する
  • 値が一致しない場合は 403 Forbidden を返す
  • 値が一致する場合は 200 OK を返す
  • 値の比較には timingSafeEqual を使用する
  • 検証用ヘッダーの値や内部設定の詳細をレスポンスへ含めない

続いて、[設定]タブの[環境変数]から以下を追加します。

キー
ORIGIN_VERIFY_VALUE CloudShell で生成した検証用ヘッダー値

0022

Lambda 関数 URL の作成

Lambda 関数の[設定]タブから[関数 URL]を開き、[関数 URL を作成]を選択します。
設定は以下のとおりです。

項目 設定値
認証タイプ NONE
CORS 未設定

NONE を選択すると、Lambda 関数 URL に対する IAM 認証は行われません。今回は Lambda 関数内で X-Origin-Verify ヘッダーを検証するため、この設定を使用します。
関数 URL を作成したら、URL を控えます。以降では、CloudShell 上で環境変数に設定します。

export FUNCTION_URL='https://xxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/'

0003

Function URL への直接アクセスを確認

CloudShell から、ヘッダーなしで Lambda 関数 URL にアクセスします。

curl -i "${FUNCTION_URL}"

以下のように 403 Forbidden が返りました。

実行結果
HTTP/1.1 403 Forbidden
Date: Mon, 03 Aug 2026 04:17:52 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 23
Connection: keep-alive
x-amzn-RequestId: xxxxxxxxxxxxxxxxxxxxx
cache-control: no-store
X-Amzn-Trace-Id: Root=1-xxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx;Parent=6xxxxxxxxxxxxe;Sampled=0;Lineage=1:3312f90a:0

{"message":"Forbidden"}~

続いて、不正な X-Origin-Verify ヘッダーを付与してアクセスします。

curl -i \
  -H 'X-Origin-Verify: invalid' \
  "${FUNCTION_URL}"

こちらも 403 Forbidden となりました。

実行結果
HTTP/1.1 403 Forbidden
Date: Mon, 03 Aug 2026 04:18:11 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 23
Connection: keep-alive
x-amzn-RequestId: xxxxxxxxxxxxxxxxxxxxx
cache-control: no-store
X-Amzn-Trace-Id: Root=1-xxxxxx-xxxxxxxxxxxxxxxxxxxxxxx;Parent=6xxxxxxxxxxxxxx9;Sampled=0;Lineage=1:3312f90a:0

{"message":"Forbidden"}~ 

この時点では、CloudFront から付与される検証用ヘッダーがないため、Lambda 関数 URL へのリクエストは拒否されます。

0004

CloudFront ディストリビューションの作成

CloudFront コンソールで[ディストリビューションを作成]を選択します。
オリジンには、先ほど作成した Lambda 関数 URL を設定します。https:// は含めず、ホスト名のみを指定します。

xxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws

主な設定は以下のとおりです。

項目 設定値
オリジン Lambda 関数 URL のホスト名
プロトコル HTTPS のみ
Origin access control 設定しない
ビューワープロトコルポリシー HTTP から HTTPS へリダイレクト
許可された HTTP メソッド GET、HEAD
キャッシュポリシー CachingDisabled

今回はリクエストごとの挙動を確認するため、AWS マネージドキャッシュポリシーの CachingDisabled を使用します。このポリシーでは最小 TTL、最大 TTL、デフォルト TTL がいずれも 0 秒であり、CloudFront のキャッシュは無効です。
ディストリビューションを作成後、ステータスが 有効 になり、デプロイが完了するまで待機します。

0005
0006

Origin Custom Header の設定

作成したディストリビューションの[オリジン]タブで、Lambda 関数 URL のオリジンを編集します。
[カスタムヘッダーを追加]から、以下を設定します。

ヘッダー名
X-Origin-Verify CloudShell で生成した検証用ヘッダー値

設定を保存し、ディストリビューションのデプロイ完了を待ちます。
CloudFront は、オリジンカスタムヘッダーとして設定したヘッダーをオリジンリクエストへ追加します。
また、ビューワーリクエストに同名のヘッダーが含まれている場合は、CloudFront が設定値で上書きしてオリジンへ転送します。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html

0007

確認してみた

CloudFront ディストリビューションのドメイン名を CloudShell の環境変数に設定します。

export CLOUDFRONT_DOMAIN='dxxxxxxxxxxxx.cloudfront.net'

まず、ヘッダーを指定せずに CloudFront 経由でアクセスします。

curl -i "https://${CLOUDFRONT_DOMAIN}/"

以下のように 200 OK が返りました。

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-store

{"message":"OK"}

次に、クライアントから不正な X-Origin-Verify ヘッダーを指定して CloudFront へアクセスします。

curl -i \
  -H 'X-Origin-Verify: invalid' \
  "https://${CLOUDFRONT_DOMAIN}/"

この場合も 200 OK が返りました。

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-store

{"message":"OK"}

0008

Lambda 関数 URL へ直接アクセスした場合、不正なヘッダーでは 403 Forbidden でした。
一方、CloudFront 経由では、クライアントが送信した X-Origin-Verify: invalid が CloudFront のオリジンカスタムヘッダー設定値で上書きされます。そのため、Lambda 関数の検証を通過して 200 OK が返ります。
検証結果は以下のとおりです。

アクセス先 クライアントが送信するヘッダー 結果
CloudFront なし 200 OK
Lambda 関数 URL なし 403 Forbidden
Lambda 関数 URL X-Origin-Verify: invalid 403 Forbidden
CloudFront X-Origin-Verify: invalid 200 OK

カスタムヘッダー方式の制約

今回の方式では、検証用ヘッダーが一致しない Lambda 関数 URL へのリクエストを拒否できます。
一方で、固定のカスタムヘッダーを使う方式には以下の制約があります。

  • 検証用ヘッダー値は共有シークレットである
  • Lambda 関数 URL と検証用ヘッダー値の両方が漏えいすると、CloudFront を経由しない直接アクセスが可能である
  • 固定値を使用するため、漏えい時には CloudFront と Lambda 環境変数の両方を更新する必要がある
  • Lambda 関数 URL の認証タイプが NONE であるため、URL 自体は公開エンドポイントである
  • この方式だけで、リクエスト元が CloudFront であることを暗号学的に証明することはできない

CloudFront 経由のアクセスだけを許可したい場合は、Lambda 関数 URL の認証タイプを AWS_IAM に変更し、OAC を使用する方式を検討してください。
OAC を使用すると、CloudFront は Lambda 関数 URL へのオリジンリクエストに SigV4 署名を付与できます。
また、Lambda 関数 URL のリソースベースポリシーでは、CloudFront サービスプリンシパルに対して、対象の CloudFront ディストリビューション ARN を AWS:SourceArn 条件で指定します。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html

なお、CloudFront のオリジンカスタムヘッダーには設定できないヘッダーがあります。
たとえば、Cache-Control、Cookie、Host、X-Amz- で始まるヘッダー、X-Edge- で始まるヘッダーなどは設定できません。
今回使用した X-Origin-Verify は、これらの制限には該当しません。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html

まとめ

CloudFront の Origin Custom Header を使用し、Lambda 関数 URL に固定の検証用ヘッダーを付与してアクセス制御を試しました。結果は以下のとおりです。

  • CloudFront 経由のリクエストは 200 OK
  • 検証用ヘッダーを持たない Lambda 関数 URL への直接アクセスは 403 Forbidden
  • CloudFront は、クライアントが送信した同名ヘッダーをオリジンカスタムヘッダーの設定値で上書きする
  • 固定ヘッダー方式は共有シークレットに依存するため、Lambda 関数 URL への直接アクセスを完全に防ぐ用途には適さない
  • 本番環境で CloudFront 以外からのアクセスをより厳格に制御する場合は、AWS_IAM と OAC の組み合わせを検討する

検証後は、不要な Lambda 関数 URL と CloudFront ディストリビューションを削除してください。特に NONE の Lambda 関数 URL を残したままにしないよう注意します。Lambda 関数 URL を CloudFront のオリジンとして利用する際の参考になれば幸いです。

参考資料

クラスメソッドオペレーションズ株式会社について

クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AIをフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026年1月 アノテーション㈱から社名変更しました

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事