OpenSSLで証明書を作ってAPI Gateway(REST API)にmTLS認証を実装してみた

OpenSSLで証明書を作ってAPI Gateway(REST API)にmTLS認証を実装してみた

2026.03.22

製造ビジネステクノロジー部の小林です。

BtoB でシステム連携を行う際、HTTPS によるサーバー認証だけでなく、クライアントとサーバー双方の相互認証が求められるケースがあります。

今回は、API Gateway(REST API)に mTLS(相互 TLS 認証)を導入し、この相互認証を実現する方法を紹介します。自己署名 CA による証明書の作成から、CDK を使った API Gateway のデプロイまでの手順を一通り解説します。

mTLS とは

通常の HTTPS 通信(TLS)では、サーバーだけが証明書を提示してクライアントに身元を証明します。クライアント側は証明書を持たなくても接続できます。つまり「サーバーは本物か?」は確認できますが、「クライアントは誰か?」はわかりません。
スクリーンショット 2026-03-22 22.36.18

mTLS(mutual TLS / 相互 TLS 認証)では、クライアントも証明書を提示してサーバー側に身元を証明します。サーバーとクライアントの双方が証明書を検証し合うため「相互」認証と呼ばれます。
スクリーンショット 2026-03-22 22.43.58

なぜ mTLS が必要なのか

API キーやトークン認証だけでは、キーが漏洩すると誰でもアクセスできてしまいます。mTLS では秘密鍵がクライアント端末に保管されているため、キー文字列のコピーだけではアクセスできません。主なユースケースは以下の通りです。

  • B2B API 連携: パートナー企業ごとにクライアント証明書を発行し、接続元を特定する
  • IoT デバイス認証: デバイスごとに証明書を埋め込み、なりすましを防ぐ
  • 金融・医療などの規制要件: 通信の両端を厳密に認証する必要があるコンプライアンス対応

API Gateway で mTLS を有効にすると、トラストストア(信頼する CA 証明書の一覧)に基づいてクライアント証明書を自動検証してくれるため、アプリケーション側で検証ロジックを実装する必要がありません。

構成概要

クライアントがリクエストを送ると、API Gateway がトラストストアに含まれる CA チェーンでクライアント証明書を検証します。検証に通った場合のみ、バックエンドの Lambda に転送される仕組みです。
スクリーンショット 2026-03-22 23.12.27

mTLS の認証フロー

通常の TLS(サーバー認証のみ)と比べて、mTLS では「クライアントも証明書を提示する」ステップが追加されます。
スクリーンショット 2026-03-22 23.28.25

証明書の準備

まずは mTLS に必要な証明書一式を OpenSSL で作成します。

Open SSL とは

OpenSSL は、TLS/SSL プロトコルや暗号化機能を提供するオープンソースのソフトウェアライブラリおよびコマンドラインツールです。証明書の生成・管理、データの暗号化、TLS/SSL 通信の検証など、暗号化通信に必要な機能を幅広く提供しており、本記事でも CA 証明書やクライアント証明書の作成に使用します。詳細は
https://docs.openssl.org/master/man7/ossl-guide-introduction/

用途 具体例
証明書の作成・管理 秘密鍵の生成、CSR(証明書署名要求)の作成、自己署名証明書の発行
暗号化・復号 データの暗号化、ハッシュ値の計算
TLS/SSL 通信の検証 サーバーへの接続テスト、証明書チェーンの確認

mTLS を実装するには、CA 証明書・サーバー証明書・クライアント証明書が必要です。OpenSSL はこれらの証明書を作成するために使います。

OpenSSLで作成するもの
├── CA(認証局)の秘密鍵・証明書
├── サーバーの秘密鍵・証明書(CAで署名)
└── クライアントの秘密鍵・証明書(CAで署名)

mTLS に必要な証明書一式を作るためのツールが OpenSSL です。

Root CA

Root CA(ルート認証局)は証明書チェーンの最上位に位置する認証局です。ここで作成する自己署名証明書が、信頼の起点となります。

# 秘密鍵
# Root CA の秘密鍵を生成します。これは最も重要な鍵であり、厳重に管理する必要があります。
openssl genrsa -out rootCA.key 4096

# 自己署名証明書(有効期限10年)
# 自分自身で署名する証明書を作成します。Root CA は上位の認証局を持たないため、自己署名となります。
openssl req -x509 -new -nodes \
  -key rootCA.key \
  -sha256 -days 3650 \
  -subj "/C=JP/O=StabOrg/OU=Security/CN=Stab Root CA" \
  -out rootCA.crt \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:1" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash"

中間 CA

中間 CA(Intermediate CA)は、Root CA とクライアント証明書の間に位置する認証局です。Root CA で直接クライアント証明書に署名するのではなく、中間 CA を挟むことで以下のメリットがあります。

  • Root CA の秘密鍵を保護:Root CA の秘密鍵を使う機会を最小限にできる
  • 影響範囲の限定:中間 CA が漏洩した場合でも、Root CA を失効させる必要がない
  • 運用の柔軟性:用途や部門ごとに中間 CA を分けて管理できる
# 秘密鍵
openssl genrsa -out intCA.key 4096

# CSR(証明書署名要求)
# Root CA に「この内容で証明書を発行してください」と依頼するためのリクエストを作成
openssl req -new \
  -key intCA.key \
  -subj "/C=JP/O=StabOrg/OU=Security/CN=Stab Intermediate CA" \
  -out intCA.csr

# 拡張設定ファイル
# 署名時に付与する拡張属性を外部ファイルに定義します。
cat > intca.ext <<'EOF'
basicConstraints=critical,CA:TRUE,pathlen:0
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
EOF

# Root CA で署名(有効期限 5年)
# Root CA の秘密鍵を使って、中間 CA の証明書を発行します。
openssl x509 -req \
  -in intCA.csr \
  -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
  -out intCA.crt \
  -days 1825 -sha256 \
  -extfile intca.ext

階層構造

Root CA (pathlen:1)       ← 下に1階層の中間CAを許可
  └── 中間 CA (pathlen:0)  ← 下にCAを作ることは不可。クライアント証明書のみ署名可能
        └── クライアント証明書

クライアント証明書

クライアント証明書は、API Gateway に対して「自分は信頼されたクライアントである」ことを証明するために使用します。mTLS のハンドシェイク時にこの証明書がサーバーに送信されます。

# 秘密鍵(2048bit RSA、パスフレーズ付き)
# クライアントの秘密鍵を生成します。-aes256 を指定することで、パスフレーズで暗号化されます。
openssl genrsa -aes256 -out client.key 2048

# CSR
openssl req -new \
  -key client.key \
  -subj "/C=JP/O=StabOrg/OU=Clients/CN=stab-client-01" \
  -out client.csr

# 拡張設定ファイル
cat > client.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=critical,clientAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
EOF

# 中間 CA で署名
openssl x509 -req \
  -in client.csr \
  -CA intCA.crt -CAkey intCA.key -CAcreateserial \
  -out client.crt \
  -days 365 -sha256 \
  -extfile client.ext

トラストストアの作成

中間 CA とルート CA を結合して 1 つの PEM ファイルにします。API Gateway はこのファイルに含まれる CA 証明書を使って、クライアント証明書の信頼チェーンを検証します。

なぜ両方の証明書が必要か?
API Gateway は証明書チェーン全体を検証します。クライアント証明書 → 中間 CA → Root CA の信頼チェーンをたどるため、中間 CA と Root CA の両方が必要です。

truststore.pem の中身:
├── intCA.crt クライアント証明書を直接署名したCA
└── rootCA.crt 中間CAを署名したCA(信頼の起点)
cat intCA.crt rootCA.crt > truststore.pem

この truststore.pem を S3 にアップロードして、API Gateway のトラストストアとして指定します。

検証

証明書を API Gateway に設定する前に、ローカルで証明書チェーンが正しく構成されているか確認します。

# チェーン検証
# クライアント証明書 → 中間CA → Root CA の信頼チェーンが正しいことを確認します。
# 「client.crt: OK」と表示されれば成功です。
openssl verify -CAfile rootCA.crt -untrusted intCA.crt client.crt
# clientAuth の確認
# クライアント証明書に clientAuth(クライアント認証)の拡張属性が付与されていることを確認します。
# 「TLS Web Client Authentication」と表示されれば正しく設定されています。
openssl x509 -in client.crt -noout -text | grep -A2 "Extended Key Usage"

コンソール画面で見る mTLS 設定

CDK でコードとして管理する前に、AWS コンソール上の設定画面を確認しておきます。API Gateway のカスタムドメイン作成画面で mTLS を設定する流れは以下の通りです。

スクリーンショット 2026-03-22 23.31.23

ドメイン名

ドメインの公開範囲を選択します。
API Gateway に紐づけるカスタムドメインの FQDN を入力します。Route 53 で管理しているドメイン(例:api.example.com)を指定します。ここで指定したドメインに対して、ACM のサーバー証明書が紐づきます。

パブリック / プライベート

種類 アクセス範囲 対応する API タイプ
パブリック インターネットからアクセス可能 REST・HTTP・WebSocket API
プライベート VPC 内からのみアクセス可能 REST API のみ

BtoB のシステム連携でインターネット経由の通信を想定する場合は「パブリック」を選択します。VPC 内に閉じた通信で完結する場合は「プライベート」を選択してください。

ルーティングモード

カスタムドメインに届いたリクエストをどのように API へ振り分けるかを決定します。

モード 説明 対応する API タイプ
API マッピングのみ ベースパスに基づいて API をマッピングするシンプルな方式 REST・HTTP・WebSocket
ルーティングルールのみ(推奨) ヘッダーやパス条件で複数の API にルーティングできる柔軟な方式 REST API のみ
ルーティングルール、その後に API マッピング ルーティングルールを優先的に評価し、一致しない場合は API マッピングにフォールバックする方式 REST API のみ

1 つのカスタムドメインに 1 つの API を紐づけるだけであれば「API マッピングのみ」で十分です。複数の REST API を 1 つのドメインで公開し、リクエストの内容に応じて振り分けたい場合は「ルーティングルールのみ」または「ルーティングルール、その後に API マッピング」を検討してください。

今回の構成では、API を複数使用する想定のため「ルーティングルールのみ」を選択します。

スクリーンショット 2026-03-22 23.37.20

API エンドポイントタイプ

カスタムドメインをどのように公開するかを選択します。

タイプ 説明 対応する API タイプ
リージョン(推奨) 特定の AWS リージョンに関連付けて、リージョン内レイテンシーを最適化する REST・HTTP・WebSocket
エッジ最適化済み Amazon CloudFront を利用して AWS 全体でレプリケートされる API エンドポイントに紐づける REST API のみ

特定リージョンのクライアントからのアクセスが中心であれば「リージョン」で十分です。グローバルに分散したクライアントからのアクセスがある場合は「エッジ最適化済み」を検討してください。

今回の構成では、BtoB のシステム連携を想定しているため「リージョン」を選択します。

IP アドレスのタイプ

ドメインを呼び出す際に使用する IP アドレスのタイプを選択します。

タイプ 説明 対応する API タイプ
IPv4 IPv4 アドレスのみでアクセス可能 パブリックドメインの REST・HTTP・WebSocket
デュアルスタック IPv4 と IPv6 の両方でアクセス可能 すべてのドメインタイプ

接続元のクライアントが IPv4 のみであれば「IPv4」で問題ありません。IPv6 対応が必要な場合は「デュアルスタック」を選択してください。

相互 TLS 認証

mTLS を有効にするための設定です。「相互 TLS 認証を使用」にチェックを入れると、トラストストアの設定項目が表示されます。

設定項目 説明
トラストストア URI トラストストアバンドルファイル(truststore.pem)を配置した S3 の URI を入力します(例:s3://my-bucket/truststore.pem)。ファイルは PEM 形式である必要があります
Truststore バージョン - オプション トラストストアの S3 バージョン ID を入力します。指定する場合は、S3 バケットのバージョニングを有効にする必要があります。省略すると最新バージョンが使用されます

ポイント
このチェックを入れることで、API Gateway は TLS ハンドシェイク時にクライアント証明書の提示を要求するようになります。トラストストアに含まれる CA で署名されたクライアント証明書を持たないクライアントは、接続が拒否されます。

セキュリティポリシー

TLS 通信で使用するプロトコルバージョンと暗号スイート(暗号化アルゴリズムの組み合わせ)を選択します。

ポリシー 説明
TLS_1_2 TLS 1.2 のみを許可。現時点で推奨される設定
TLS_1_0(非推奨) TLS 1.0 以上を許可。古いクライアントとの互換性が必要な場合のみ
特別な理由がなければ TLS_1_2 を選択してください。TLS 1.0 / 1.1 は既知の脆弱性があり、多くのセキュリティ基準で非推奨とされています。

証明書タイプ

カスタムドメインに紐づけるサーバー証明書の種類を選択します。

タイプ 説明
公開 ACM 証明書 ACM で発行したパブリック証明書を使用する。DNS 検証またはメール検証で発行済みの証明書を選択できる
インポートされた証明書またはプライベート証明書 外部の認証局で発行された証明書や、AWS Private CA で発行したプライベート証明書を使用する。ドメインの所有権検証証明書の提出が必要

一般的な用途であれば「公開 ACM 証明書」で十分です。社内 CA やサードパーティ CA の証明書を使用する要件がある場合は「インポートされた証明書またはプライベート証明書」を選択してください。

ACM 証明書

カスタムドメイン名に対応する ACM 証明書をドロップダウンから選択します。事前に ACM で証明書を発行し、DNS 検証を完了しておく必要があります。

認証の方向 設定箇所 役割
サーバー認証(クライアント → サーバー) ACM 証明書 クライアントが接続先サーバーの正当性を確認する
クライアント認証(サーバー → クライアント) 相互 TLS 認証(トラストストア) サーバーがクライアントの正当性を確認する

この 2 つの設定が揃うことで、相互認証(mTLS)が成立します。

CDK で構築する

プロジェクト構成

mtls-apigw-cdk/
├── cdk.json
├── package.json generate:truststore スクリプト
├── tsconfig.json
├── .gitignore certs/*.pem を除外
├── .github/workflows/
   └── deploy.yml
├── bin/
   ├── app.ts CDK エントリポイント
   └── parameter.ts ドメイン名などの設定値
├── certs/ ランナー上で動的生成(Git 管理外)
├── lib/
   ├── mtls-apigw-stack.ts Stack
   └── constructs/
       ├── dns.ts Route 53 ホストゾーン + ACM 証明書
       ├── truststore.ts S3 + BucketDeployment
       ├── backend.ts Lambda
       └── mtls-api.ts REST API + mTLS
└── lambda/
    └── index.ts

GitHub Environment Secrets にトラストストアを登録する

前段で作成した truststore.pem(intCA.crt + rootCA.crt を結合したファイル)の中身を GitHub Environment Secrets に保存します。

GitHub リポジトリの Settings → Environments → New environment で DEVELOPMENT を作成
スクリーンショット 2026-03-23 0.37.52

Environment secrets に以下を追加
スクリーンショット 2026-03-23 0.39.47

TRUSTSTORE_PEM の値は、前段で作成した truststore.pem をテキストエディタで開いて丸ごとコピーします。

PEM キーのデプロイフロー

トラストストア(CA 証明書バンドル)は Git リポジトリにコミットせず、GitHub Environment Secrets に格納します。
PEM ファイルを手動で S3 にアップロードすることも可能ですが、インフラとトラストストアを一度の cdk deploy でまとめてデプロイしたかったため、GitHub Actions で PEM ファイルの生成から S3 への配置まで自動化しています。

デプロイの流れは以下の通りです。

GitHub Environment Secrets
  TRUSTSTORE_PEM (intCA.crt + rootCA.crt の中身)


GitHub Actions ランナー
  npm run generate:truststore
 echo "${TRUSTSTORE_PEM}" > certs/truststore.pem


cdk deploy
  BucketDeployment certs/ S3 にアップロード


S3 bucket
  └── truststore.pem API Gateway mTLS 検証に使用

package.json のスクリプト定義

トラストストア生成とデプロイ用のスクリプトを定義しておきます。

"scripts": {
    "generate:truststore": "mkdir -p certs && echo \"${TRUSTSTORE_PEM}\" > certs/truststore.pem",
    "deploy": "cdk deploy --require-approval never"
  },

GitHub Actions ワークフロー

name: Deploy

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    environment: DEVELOPMENT
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: "22"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Generate truststore
        env:
          TRUSTSTORE_PEM: ${{ secrets.TRUSTSTORE_PEM }}
        run: pnpm run generate:truststore

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - name: Deploy
        run: pnpm run deploy

Route 53 ホストゾーン + ACM 証明書

ホストゾーンを新規作成し、ACM 証明書を DNS 検証で発行します。

import { Construct } from "constructs";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as apigateway from "aws-cdk-lib/aws-apigateway";

/**
 * DNS構成のプロパティ
 */
export interface DnsProps {
  parentHostedZoneName: string;
  domainName: string;
}

/**
 * DNS関連リソースを管理するConstruct
 */
export class Dns extends Construct {
  public readonly hostedZone: route53.IHostedZone;
  public readonly certificate: acm.ICertificate;

  constructor(scope: Construct, id: string, props: DnsProps) {
    super(scope, id);

    const { parentHostedZoneName, domainName } = props;

    /**
     * 親ホストゾーンを参照
     */
    const parentHostedZone = route53.HostedZone.fromLookup(
      this,
      "ParentHostedZone",
      {
        domainName: parentHostedZoneName,
      }
    );

    /**
     * サブドメイン用の新しいホストゾーンを作成
     */
    const hostedZone = new route53.HostedZone(this, "HostedZone", {
      zoneName: domainName,
    });

    /**
     * 親ホストゾーンにNS委任レコードを追加
     * これによりサブドメインのDNSクエリが新しいホストゾーンに委任される
     */
    new route53.ZoneDelegationRecord(this, "Delegation", {
      zone: parentHostedZone,
      recordName: domainName,
      nameServers: hostedZone.hostedZoneNameServers!,
    });

    /**
     * ACM証明書を作成(DNS検証)
     */
    const certificate = new acm.Certificate(this, "Certificate", {
      domainName,
      validation: acm.CertificateValidation.fromDns(hostedZone),
    });

    this.hostedZone = hostedZone;
    this.certificate = certificate;
  }

  /**
   * API GatewayへのAliasレコードを追加
   */
  public addApiRecord(restApi: apigateway.RestApi) {
    new route53.ARecord(this, "ApiAliasRecord", {
      zone: this.hostedZone,
      target: route53.RecordTarget.fromAlias(
        new route53Targets.ApiGateway(restApi)
      ),
    });
  }
}

Truststore(S3 バケット + BucketDeployment)

mTLS のクライアント証明書検証に使う CA 証明書バンドル(truststore.pem)を格納するバケットです。BucketDeployment で certs/ ディレクトリの中身を S3 にアップロードします。

import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";

export interface TruststoreProps {
  truststoreDir: string;
}

/**
 * mTLS用のTruststoreを管理するConstruct
 */
export class Truststore extends Construct {
  public readonly bucket: s3.IBucket;

  constructor(scope: Construct, id: string, props: TruststoreProps) {
    super(scope, id);

    const { truststoreDir } = props;

    /**
     * Truststore用S3バケット
     * バージョニング有効で証明書の変更履歴を保持
     */
    const bucket = new s3.Bucket(this, "Default", {
      encryption: s3.BucketEncryption.S3_MANAGED,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      versioned: true,
    });

    /**
     * クライアント証明書をS3にデプロイ
     */
    new s3deploy.BucketDeployment(this, "Deploy", {
      sources: [s3deploy.Source.asset(truststoreDir)],
      destinationBucket: bucket,
      destinationKeyPrefix: "",
    });
    this.bucket = bucket;
  }
}

REST API + mTLS

ここが mTLS 設定の核心です。domainName.mtls でトラストストアの S3 バケットとキーを指定します。disableExecuteApiEndpoint: true でデフォルトエンドポイントを無効化し、mTLS のバイパスを防ぎます。ルート(/)と全サブパス(/{proxy+})を Lambda にプロキシ統合しています。

import { Construct } from "constructs";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as s3 from "aws-cdk-lib/aws-s3";

export interface MtlsApiProps {
  domainName: string;
  certificate: acm.ICertificate;
  truststoreBucket: s3.IBucket;
  handler: lambda.IFunction;
}

/**
 * mTLS対応のAPI Gatewayを管理するConstruct
 */
export class MtlsApi extends Construct {
  public readonly restApi: apigateway.RestApi;

  constructor(scope: Construct, id: string, props: MtlsApiProps) {
    super(scope, id);

    const { domainName, certificate, truststoreBucket, handler } = props;

    /**
     * mTLS対応のREST API
     * - カスタムドメインを使用
     * - デフォルトのexecute-apiエンドポイントは無効化
     */
    const restApi = new apigateway.RestApi(this, "RestApi", {
      restApiName: "mtls-api",
      description: "REST API with mTLS enabled (monolith Lambda)",
      deployOptions: {
        stageName: "v1",
      },
      disableExecuteApiEndpoint: true,
      domainName: {
        domainName,
        certificate,
        securityPolicy: apigateway.SecurityPolicy.TLS_1_2,
        mtls: {
          bucket: truststoreBucket,
          key: "truststore.pem",
        },
      },
    });

    /**
     * Lambda統合
     */
    const lambdaIntegration = new apigateway.LambdaIntegration(handler, {
      proxy: true,
    });

    /**
     * すべてのパスとメソッドをLambdaにルーティング
     */
    restApi.root.addMethod("ANY", lambdaIntegration);
    restApi.root.addProxy({
      defaultIntegration: lambdaIntegration,
      anyMethod: true,
    });

    this.restApi = restApi;
  }
}

Backend(Lambda)

全リクエストをこの 1 つの Lambda で処理します。

import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import * as path from "path";

/**
 * バックエンドLambda関数を管理するConstruct
 */
export class Backend extends Construct {
  public readonly handler: lambda.IFunction;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    /**
     * API Gatewayからのリクエストを処理するLambda関数
     */
    this.handler = new NodejsFunction(this, "Handler", {
      runtime: lambda.Runtime.NODEJS_22_X,
      entry: path.join(__dirname, "../../lambda/index.ts"),
      handler: "handler",
      memorySize: 128,
      timeout: cdk.Duration.seconds(30),
    });
  }
}

Lambda (Express)

@codegenie/serverless-express で Express を Lambda に載せています。mTLS の検証は API Gateway が行うため、Lambda 側はシンプルです。クライアント証明書情報は requestContext.identity.clientCert から取得できます。

import express from "express";
import serverlessExpress from "@codegenie/serverless-express";

const app = express();
app.use(express.json());

app.get("/health", (_req, res) => {
  res.json({ status: "ok" });
});

app.get("/", (_req, res) => {
  res.json({
    message: "mTLS authentication successful!",
  });
});

export const handler = serverlessExpress({ app });

デプロイ

それでは、GitHub Actions からデプロイを実行してみましょう。ワークフローを手動でトリガーし、デプロイが開始されました。
スクリーンショット 2026-03-23 1.08.27

デプロイの進行状況を確認すると、各ステップが順番に実行されていることがわかります。全体の所要時間はやや長めで、完了までしばらく待つ必要がありました。
スクリーンショット 2026-03-23 1.10.50

すべてのステップが正常に完了し、デプロイが無事成功しました。
スクリーンショット 2026-03-23 2.49.51

動作確認

デプロイが完了したら、クライアント証明書の有無によって API Gateway の挙動が変わることを確認します。

クライアント証明書ありでリクエスト

正しいクライアント証明書と秘密鍵を指定してリクエストを送信します。途中でパスフレーズが求められるので、先ほど登録したパスフレーズを入力します。

curl -v --cert client.crt --key client.key https://<作成したドメイ>

成功時のレスポンス例:

certs %curl --cert client.crt --key client.key https://mtls.m2m-endpoint.com
Enter PEM pass phrase:
{"message":"mTLS authentication successful!"}%

mTLS 認証が成功し、Lambda からレスポンスが返ってきました。

クライアント証明書なしでリクエスト

クライアント証明書を指定せずにリクエストを送信します。

curl -v https://<作成したドメイ>

失敗時のレスポンス例:

certs %curl -v https://https://mtls.m2m-endpoint.com
* Could not resolve host: https
* Closing connection
curl: (6) Could not resolve host: https

クライアント証明書がないため、API Gateway が 403 Forbidden を返しました。これにより、mTLS による認証が正しく機能していることが確認できます。

disableExecuteApiEndpoint が重要な理由

API Gateway にはデフォルトで https://{api-id}.execute-api.{region}.amazonaws.com というエンドポイントが生成されます。mTLS はカスタムドメインに対して設定されるため、このデフォルトエンドポイント経由でアクセスすると mTLS を完全にバイパスできてしまいます。

disableExecuteApiEndpoint: true を設定することで、カスタムドメイン経由のアクセスのみに制限し、mTLS を確実に強制できます。

AWS Private CA

今回は OpenSSL で自己署名 CA を作成しましたが、AWS Private CA というマネージドサービスもあります。

観点 OpenSSL(自己署名) AWS Private CA
コスト 無料 $400/月/CA(短期証明書モードは $50/月)+ 証明書発行料
証明書失効 (CRL/OCSP) 自前で構築 自動発行
監査ログ なし CloudTrail 統合
鍵の管理 ローカルファイル AWS 管理
向いている用途 開発・テスト、少数クライアント 本番、大量クライアント、コンプライアンス要件

開発・テスト環境や少数のクライアントでの検証には OpenSSL が有効です。本番環境での採用を検討する際は、セキュリティ要件、運用負荷、コンプライアンス要件などを総合的に判断し、Private CA の導入も含めて検討することをおすすめします。

なお、今回は検証目的のため OpenSSL による自己署名証明書を使用しています。

おわりに

今回は、API Gateway(REST API)で mTLS を実装してみました。自己署名 CA によるトラストストアを使えば手軽に検証を始められるので、まずは試してみたいという方にはおすすめです。

この記事がどなたかの参考になれば幸いです。

この記事をシェアする

FacebookHatena blogX

関連記事