相互 TLS 認証で API Gateway のバックエンドの S3 にアクセスできるかやってみた

こんにちは、アノテーション テクニカルサポートチームの中野です。
API Gateway のバックエンドに S3 を設定するパターンと API Gateway の通信で相互 TLS 認証(mTLS)を行うパターンの組み合わせを試す機会があったので、以下に手順をまとめてみました。

構成

以下のような構成で構築してみます。

また、mTLS の詳しい仕組みについては、以下がわかりやすかったので参照ください。

やってみた

前提条件

構築するまえに、前提として API Gateway のカスタムドメインを作成するために、以下を準備しておきます。

  • Route53 へのドメインの登録
  • ACM で証明書発行

準備が完了したら、以下手順を進めてきます。

ステップ 1: S3 に静的ファイルをアップロード

まず、S3 を作成します。
何らかの適当な文字を描画する HTML ファイルをアップロードしておきます。

また、API Gateway から今回作った S3 のみにアクセスできるように、以下のような IAM ポリシーを作成しておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Resource": [
                "arn:aws:s3:::<S3 バケット名>",
                "arn:aws:s3:::<S3 バケット名>/*"
            ],
            "Effect": "Allow"
        }
    ]
}

このポリシーで、API Gateway が信頼されたエンティティ(ロールをアタッチできる対象)となるように IAM ロールを作成します。
IAM ロールは、ステップ 2 の API Gateway を構築する際に利用します。

ステップ 2: API Gateway の構築

では、S3 へプロキシする API Gateway を作成します。
API のタイプは、REST API で作成していきます。

リソースを新たに作成します。 ここでは、/page というパスで作成しました。

リソースに対して GET メソッドを追加して、以下のような設定を行います。
注意点として、「パスの上書きの使用」の部分に {S3 バケット名}/{取得するファイル名} となるように記載します。
また、実行ロール部分は、ステップ 1 で作成した API Gateway から S3 へのアクセスの許可を行うための IAM ロールの ARN を記載します。

作成が完了したら、適切なステージ名でデプロイします。

なお、ここで API Gateway のデフォルトのエンドポイントを無効化しておきます。
デフォルトのエンドポイントを無効化していない状態で、相互 TLS 認証を API Gateway へ設定すると、クライアント証明書なしでのアクセスができてしまいますので、運用前に見落としがないように注意してください。

再度、設定を反映するために、再度ステージへデプロイしておきます。

ステップ 3: カスタムドメインの作成

カスタムドメインを作成します。

このとき、一旦相互 TLS 認証を無効にしたまま、ドメイン作成を実行します。

ステップ 4: クライアント証明書の発行

クライアント証明書を自分のローカルマシンで作成します。

以下のブログの手順で実行しました。

ルート CA の秘密鍵を作成します。

$ openssl genrsa -out RootCA.key 4096
Generating RSA private key, 4096 bit long modulus
......................................................................................................................................................++
..................................++
e is 65537 (0x10001)

続いて、自己署名 CA 証明書作成します。

$ openssl req -new -x509 -days 36500 -key RootCA.key -out RootCA.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) []:JP
State or Province Name (full name) []:FUKUOKA
Locality Name (eg, city) []:
Organization Name (eg, company) []:Annotation
Organizational Unit Name (eg, section) []:Technical Support
Common Name (eg, fully qualified host name) []:an-nakano ca
Email Address []:

クライアント側の秘密鍵を作成します。

$ openssl genrsa -out my_client.key 2048
Generating RSA private key, 2048 bit long modulus
.............................................................................................................................+++
.....................................+++
e is 65537 (0x10001)

秘密鍵をもとに CSR を作成します。

$ openssl req -new -key my_client.key -out my_client.csr
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) []:JP
State or Province Name (full name) []:FUKUOKA
Locality Name (eg, city) []:
Organization Name (eg, company) []:Annotation
Organizational Unit Name (eg, section) []:Technical Support
Common Name (eg, fully qualified host name) []:an-nakano client
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:

最後にクライント証明書を作成します。

$ openssl x509 -req -in my_client.csr -CA RootCA.pem -CAkey RootCA.key -set_serial 01 -out my_client.pem -days 36500 -sha256
Signature ok
subject=/C=JP/ST=FUKUOKA/O=Annotation/OU=Technical Support/CN=an-nakano client
Getting CA Private Key

作成した CA 証明書を適当な S3 バケットに保存します。

$ aws s3 cp RootCA.pem s3://<CA証明書用S3バケット名>/
upload: ./RootCA.pem to s3://<CA証明書用S3バケット名>/RootCA.pem

ステップ 5: API Gateway で相互 TLS 認証を設定

では、ステップ 3 で作成したカスタムドメインに、ステップ 4 で作成した S3 にアップロード済みの CA 証明書を設定します。

カスタムドメインのステータスが「利用可能」になるまで待ちます。

最後に、カスタムドメインの API マッピング設定で、API Gateway のステージを関連付けます。

ここで、Route53 のエイリアスレコードで API Gateway へトラフィック転送するように設定して完了です。

ステップ 6: 検証

では、クライアント証明書ありで、curl でアクセスしてみます。

$ curl -v -i --key my_client.key --cert my_client.pem  https://api.example.com/page
*   Trying 54.249.165.209...
* TCP_NODELAY set
* Connected to api.example.com (54.249.165.209) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=example.com
*  start date: Nov 15 00:00:00 2021 GMT
*  expire date: Dec 14 23:59:59 2022 GMT
*  subjectAltName: host "api.example.com" matched cert's "*.example.com"
*  issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7ffd2f008200)
> GET /page HTTP/2
> Host: api.example.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200
HTTP/2 200
< x-amzn-requestid: a0469407-1a6b-4472-xxxxxxxxxx
x-amzn-requestid: a0469407-1a6b-4472-xxxxxxxxxx
< x-amz-apigw-id: M_xxxxxxxxxx
x-amz-apigw-id: M_xxxxxxxxxx
< x-amzn-trace-id: Root=1-61fc83ab-xxxxxxxxxx
x-amzn-trace-id: Root=1-61fc83ab-xxxxxxxxxx
< content-type: application/json
content-type: application/json
< content-length: 38
content-length: 38
< date: Fri, 04 Feb 2022 01:38:51 GMT
date: Fri, 04 Feb 2022 01:38:51 GMT

<
<html>
  Hello World!! Yeah!!
</html>
* Connection #0 to host api.example.com left intact
* Closing connection 0

無事、アクセスできることを確認しました。

次に、クライアント証明書なしで、curl でアクセスしてみます。

$ curl -v -i https://api.example.com/page
*   Trying 18.180.7.23...
* TCP_NODELAY set
* Connected to api.example.com (18.180.7.23) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to api.example.com:443
* Closing connection 0
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to api.example.com:443

アクセスされないことが確認できました。

最後に

API Gateway で S3 へ簡単にプロキシできる環境を構築できて、さらに API Gateway の認証として相互 TLS 認証を利用する例をまとめてみました。
クライアント証明書をもつユーザのみにサイトを公開するようなパターンに有用なのではないでしょうか。

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社 WEB サイトをご覧ください。