9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWSとその不確かな壁(1) ~ALBとmTLS認証(クライアント証明書パススルー)編~

Last updated at Posted at 2025-11-04

本記事は、BIPROGY / ユニアデックス社内AWSコミュニティ「BIPROGY AWS Ambassador」の
定期投稿企画第5回目の記事です。他の定期投稿企画の記事は、
#BIPROGY_AWS_Ambassadorタグ または Organizationページをご覧ください。

1. はじめに

AWS上に築く城壁都市(仮想プライベートクラウド)とその壁の外との境界として、どんな門があるかを見ていくシリーズ第一弾です。

n02.jpg

1.1 壁と門

AWSの城壁(ファイアウォール)といえば、セキュリティグループやAWS Network Firewall、AWS WAFなどが挙げられます。城門(ゲートウェイ)としては、Internet GatewayやVPN Gatewayなどがあるでしょう。その門を通るときの通行許可として、AWSではどのようなサービスが使われているのでしょうか。
(Fargateというサービスには、gateという単語がついていますが、Docker実行をAWS側で管理・支援するという機能ですので、門ではないですね。中世でいえば、「商人・職人」(=Dockerコンテナ)を支える「ギルド」(=コンテナ基盤)がFargateでしょうか。)

1.2 通行許可

さて、前置きが長くなりました。通行許可(証明書やアカウントによる認証)の方法といえば、AWS Client VPNやAmazon Cognitoなどもありますが、第一弾は、ニッチな方法であるALBのmTLS認証について見ていきたいと思います。今回は、認証の仕組みを理解するため、ALBでは認証はせず、裏側のアプリケーションで認証させるようにします。外側の門は通して、門の中で通行許可証を検証するイメージでしょうか。

Amazon Web Services ブログに、mTLSについて、およびALBでの実装内容についての説明があります。まずは、こちらを一読することをおすすめします。

2. 環境構成

構成はシンプルに、ALBとEC2を使い、ACMに自己証明書を配置する、とします。

Arch03.png

2.1 クライアントPCで証明書の作成

検証で使用する証明書は、Windows+OpenSSLという環境で作成しました。
TLS暗号はALBで終端させる構成でもあるため、サーバ証明書も作成します。

この記事では、ALB mTLSの動作確認のみの目的で、検証環境用にプライベートCA、自己署名の証明書を使用しています。

2.1.1 プライベート認証局(CA)の秘密鍵と自己署名の証明書を作成

PS> openssl genrsa -aes256 -out ca-key.pem 2048
Enter PEM pass phrase:

PS> openssl req -new -x509 -key ca-key.pem -out ca-cert.pem -days 365

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4236903/746b5196-b231-4f46-8816-e53302fbf95f.png)
Enter pass phrase for ca-key.pem:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Kyoto
Locality Name (eg, city) []:Kyoto
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Private UAL Ltd.
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:CA Private.Local
Email Address []:

  • ca-key.pem:CAの秘密鍵
  • ca-cert.pem:CA自己署名証明書(EC2にも配置する)

2.1.2 サーバ秘密鍵とサーバ証明書署名要求(CSR)の作成

PS> openssl genrsa -out server-key.pem 2048
PS> openssl req -new -key server-key.pem -out server-req-san.pem -addext "subjectAltName=DNS:bipual-dev.net"

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4236903/a22e5b70-41dd-4921-9062-e902b9da1803.png)
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Osaka
Locality Name (eg, city) []:Kita Ku
Organization Name (eg, company) [Internet Widgits Pty Ltd]:BIPUAL Ltd.
Organizational Unit Name (eg, section) []:AWS Team
Common Name (e.g. server FQDN or YOUR name) []:bipual-dev.net
Email Address []:

サブジェクト代替名(SAN)拡張にFQDN名を入れておくと、Chromeで証明書エラーとなりません(2025/10時点)。

2.1.3 CAでサーバ証明書に署名

PS> openssl x509 -req -in server-req-san.pem -out server-cert-san.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -days 365 -copy_extensions copy
Certificate request self-signature ok
subject=C=JP, ST=Osaka, L=Kita Ku, O=BIPUAL Ltd., OU=AWS Team, CN=bipual-dev.net
Enter pass phrase for ca-key.pem:
  • server-key.pem:サーバ秘密鍵(AWS ACMに設定)
  • server-cert-san.pem:サーバ証明書(AWS ACMに設定)

2.1.4 クライアント秘密鍵とクライアント証明書署名要求(CSR)を作成

PS> openssl genrsa -out client-key.pem 2048
PS> openssl req -new -key client-key.pem -out client-req.pem -subj "/C=JP/ST=Osaka/O=AWSTeam/CN=bipual-client1"

2.1.5 CAでクライアント証明書に署名

PS> openssl x509 -req -in client-req.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365 -sha256
Certificate request self-signature ok
subject=C=JP, ST=Osaka, O=AWSTeam, CN=bipual-client1

2.1.6 証明書の検証

PS> openssl verify -CAfile ca-cert.pem client-cert.pem
client-cert.pem: OK

2.1.7 クライアント証明書をPKCS#12形式で作成

PS> openssl pkcs12 -export -out client.pfx -inkey client-key.pem -in client-cert.pem -certfile ca-cert.pem -passout pass:
  • client.pfx:PKCS#12形式のクライアント証明書(OSの証明書ストアにインポート)

2.2 クライアントOSの証明書ストアにルート証明書をインポート

ここでは、Windows環境での証明書のインポートについて説明します。

2.2.1 CA証明書を信頼された証明機関のストアに登録

certmgr(ユーザー証明書の管理)の証明書インポートウィザードでインポートします。

  • ca-cert.pem:CA自己署名証明書

certmgr02.png

2.2.2 hostsファイルにALBのパブリックIPアドレスを登録

ALBのパブリックDNS名(FQDN名)で解決されるグローバルIPアドレスをhostsファイルに書いておきます。
Windowsでは、"C:\Windows\System32\drivers\etc\hosts"にhostsファイルがあります。

54.64.xx.xxx bipual-dev.net
52.199.xxx.xx bipual-dev.net	

2.2.3 クライアント証明書のインポート

  • client.pfx:PKCS#12形式のクライアント証明書

証明書をダブルクリックし、証明書のインポートウィザードを起動して、証明書ストアに格納します。

ClientCert01.png

2.3 AWS Certificate Manager (ACM)へ証明書を登録

証明書をインポートします。

ACM01.png

  • 証明書本文:server-cert-san.pem
  • 証明書のプライベートキー:server-key.pem
  • 証明書チェーン - オプション:(今回は使用しない)

2.4 AWS ALBを作成

AWS Application Load Balancer(ALB) を作成します。

  • 基本的な設定
    • ロードバランサー名:maehara-dev-alb01
    • スキーム:インターネット向け
  • ネットワークマッピング
    • VPC
    • アベイラビリティーゾーンとサブネット
  • セキュリティグループ:(設定する)
  • リスナーとルーティング
    • プロトコル:HTTPS
    • アクションのルーティング:ターゲットグループへ転送
      • ターゲットグループ:上記で構築したEC2をターゲットしたターゲットグループ
  • セキュアリスナーの設定
    • デフォルト SSL/TLS サーバー証明書
      • 証明書の取得先:ACMから
      • 証明書 (ACM から):ACMでインポートした証明書
  • クライアント証明書の処理
    • 相互認証 (mTLS):パススルー

ALB01.png

ALB02.png

2.5 AWS EC2にアプリケーションサーバーを設定

クライアント証明書をパススルーするサーバーとして、以下のEC2をセットアップします。

  • Amazon Linux2023 プライベートサブネットに配置
  • Apache+PHPを導入
  • /etc/ssl/certs/ca-cert.pem に上記のCA証明書ファイルを配置

ALBを経由して、このサーバーに接続すると、ブラウザが認識する証明書は以下のようになります。

Chrome-cert02.png

2.6 EC2上のアプリケーションをPHPで作成

ALBを通じて、ターゲットとなるEC2インスタンス上で動作するPHPのWebアプリケーションを使い、クライアント証明書の中身を確認してみます。
アプリケーションは、クラスメソッドさんが素晴らしい記事を作成されておりますので、その記事内に記載されたPHPプログラムをベースに、HTTPのヘッダーやクライアント証明書のデータを確認する処理を追加しました。

index.php
<?php

// HTTPリクエストヘッダーからx-amzn-mtls-clientcertを取得
$mtlsClientCert = $_SERVER['HTTP_X_AMZN_MTLS_CLIENTCERT'] ?? '';

if (empty($mtlsClientCert)) {
    echo 'No client certificate provided';
    exit;
}
echo "<h2>HTTPヘッダー: HTTP_X_AMZN_MTLS_CLIENTCERT</h2>";
echo "<code>" . htmlspecialchars($mtlsClientCert) . "</code>";

// URLエンコードされている証明書をデコード
$decorded = rawurldecode($mtlsClientCert);
echo "<h2>URLデコードしたクライアント証明書(一部省略)</h2><code>";
$ccert = htmlspecialchars($decorded);
echo  mb_substr($ccert, 0, 70, 'UTF-8') . "..." . mb_substr($ccert, -70, null, 'UTF-8');
echo "</code>";

// クライアント証明書の検証
$cert = openssl_x509_parse($ccert);
if ($cert === false) {
    echo 'Invalid client certificate';
    exit;
}

// クライアント証明書のCNが正しいかを検証
if ($cert['subject']['CN'] !== 'bipual-client1') {
    echo 'Invalid client';
    exit;
}

// クライアント証明書の有効期限を検証
$now = time();
if ($now < $cert['validFrom_time_t'] || $now > $cert['validTo_time_t']) {
    echo 'Client certificate is expired';
    exit;
}

// クライアント証明書の発行者が自己署名CAであることを検証
if ($cert['issuer']['CN'] !== 'CA Private.Local') {
    echo 'Invalid issuer';
    exit;
}

// CA証明書を取得
$caCert = file_get_contents('/etc/ssl/certs/ca-cert.pem');
if ($caCert === false) {
    echo "Failed to read CA certificate\n";
    exit;
}

// 署名の検証
echo "<h2>署名の検証</h2>";
$result = openssl_x509_verify($ccert, $caCert);
if ($result === 1) {
    echo "Client certificate is valid\n";
} elseif ($result === 0) {
    echo "Client certificate is not valid\n";
} else {
    echo "Failed to verify client certificate\n";
    while ($msg = openssl_error_string()) {
        echo $msg . "\n";
    }
}

// 証明書の内容をダンプ
echo "<h2>parsed cert </h2><pre>";
var_dump($cert);
echo "</pre>";

このように、HTTPのリクエストヘッダー:HTTP_X_AMZN_MTLS_CLIENTCERTにクライアント証明書のデータがURLエンコードされた状態で渡されるので、これをURLデコード(PHPではrawurldecode)します。openssl_x509_parse関数で連想配列に読み込むことができます。

以下は、配列のダンプデータです。

array(16) {
  ["name"]=>
  string(42) "/C=JP/ST=Osaka/O=AWSTeam/CN=bipual-client1"
  ["subject"]=>
  array(4) {
    ["C"]=>
    string(2) "JP"
    ["ST"]=>
    string(5) "Osaka"
    ["O"]=>
    string(7) "AWSTeam"
    ["CN"]=>
    string(14) "bipual-client1"
  }
  ["hash"]=>
  string(8) "e2ccf8f2"
  ["issuer"]=>
  array(5) {
    ["C"]=>
    string(2) "JP"
    ["ST"]=>
    string(5) "Kyoto"
    ["L"]=>
    string(5) "Kyoto"
    ["O"]=>
    string(16) "Private UAL Ltd."
    ["CN"]=>
    string(16) "CA Private.Local"
  }
  ["version"]=>
  int(2)
  ["serialNumber"]=>
  string(42) "0x0123456789ABCDEF0123456789ABCDEF01234567"
  ["serialNumberHex"]=>
  string(40) "0123456789ABCDEF0123456789ABCDEF01234567"
  ["validFrom"]=>
  string(13) "251028120832Z"
  ["validTo"]=>
  string(13) "261028120832Z"
  ["validFrom_time_t"]=>
  int(1761653312)
  ["validTo_time_t"]=>
  int(1793189312)
  ["signatureTypeSN"]=>
  string(10) "RSA-SHA256"
  ["signatureTypeLN"]=>
  string(23) "sha256WithRSAEncryption"
  ["signatureTypeNID"]=>
  int(668)
  ["purposes"]=>
  array(10) {
    [1]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(false)
      [2]=>
      string(9) "sslclient"
    }
    [2]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(false)
      [2]=>
      string(9) "sslserver"
    }
    [3]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(false)
      [2]=>
      string(11) "nssslserver"
    }
    [4]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(false)
      [2]=>
      string(9) "smimesign"
    }
    [5]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(false)
      [2]=>
      string(12) "smimeencrypt"
    }
    [6]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(false)
      [2]=>
      string(7) "crlsign"
    }
    [7]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(true)
      [2]=>
      string(3) "any"
    }
    [8]=>
    array(3) {
      [0]=>
      bool(true)
      [1]=>
      bool(false)
      [2]=>
      string(10) "ocsphelper"
    }
    [9]=>
    array(3) {
      [0]=>
      bool(false)
      [1]=>
      bool(false)
      [2]=>
      string(13) "timestampsign"
    }
    [10]=>
    array(3) {
      [0]=>
      bool(false)
      [1]=>
      bool(false)
      [2]=>
      string(8) "codesign"
    }
  }
  ["extensions"]=>
  array(2) {
    ["subjectKeyIdentifier"]=>
    string(59) "A0:B1:C2:D3:E4:F5:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
    ["authorityKeyIdentifier"]=>
    string(59) "A0:B1:C2:D3:E4:F5:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
  }
}

3. おわりに

上記の例では、独自のプライベートCAを作って、パススルーモードを使ってアプリケーション側で認証を行いましたが、ALB側でトラストストアを作り、プライベートCAとしてAWS Private Certificate Authorityを利用するという構成も可能です。
このようにAWSのALBでは、クライアント証明書(通行証)を使って、安全に門から通行させてあげる仕組みを持っているということが確認できました。

AWSのVPCは、虚構の城塞都市であり、貿易の活発な都市もあれば、要塞化され秘密の地下道しか通じていないような都市もあるでしょう。
次回は、別の城門について見ていきたいと思います。

4. 参考

We Are Hiring!

9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?