API Gatewayにクライアント証明書による認証を設定してみる

API Gatewayがクライアント証明書による認証に対応しました。IoT機器からAPIを実行するようなユースケースで便利に使えそうです。
2020.09.21

CX事業本部@大阪の岩田です。

2020/9/17付けのアップデートによりAPI Gatewayでクライアント証明書による認証がサポートされるようになりました。

すでにAWSブログの方でクライアント証明書による認証を利用する手順が紹介されているので、こちらのブログを参考にしつつクライアント証明書による認証を設定してみたいと思います。

前提条件

クライアント証明書による認証を設定するためにはAPI Gatewayのカスタムドメイン名が必須となります。以下の記事を参考にカスタムドメインを設定したAPI Gatewayを用意しておいて下さい。

カスタムドメインが設定できたら、一度カスタムドメインを利用してAPIが実行できることを確認しておきましょう。

$ curl -s  -o /dev/null -w '%{http_code}\n' https://<設定したカスタムドメイン名>
200

200OKが返ってくれば準備完了です。

クライアント証明書の発行

ここからはクライアント証明書による認証を設定するためにクライアント証明書を発行していきます。

CAの構築

まずはルートCAを構築します。

ルート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) []:OSAKA
Locality Name (eg, city) []:HIGOBASHI
Organization Name (eg, company) []:Classmethod
Organizational Unit Name (eg, section) []:CX Division
Common Name (eg, fully qualified host name) []:cm-iwata ca
Email Address []:

CAの名前は「cm-iwata ca」としました。今回は特に中間CAは利用しないので、以上でCAの準備は完了です。

クライアントの秘密鍵とCSRの作成

続いてクライアント証明書を準備していきます。

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

$ 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) []:OSAKA
Locality Name (eg, city) []:HIGOBASHI
Organization Name (eg, company) []:Classmethod
Organizational Unit Name (eg, section) []:CX Division
Common Name (eg, fully qualified host name) []:cm-iwata client
Email Address []:

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

クライアントの名前は「cm-iwata client」としました。

クライアント証明書の発行

準備ができたのでクライアント証明書を発行します。

$ 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=OSAKA/L=HIGOBASHI/O=Classmethod/OU=CX Division/CN=cm-iwata client
Getting CA Private Key

これで準備OKです。

API Gatewayにクライアント証明書による認証を設定

ここからAPI Gatewayにクライアント証明書による認証を設定していきます。

ルートCA証明書をS3にアップ

クライアント証明書の妥当性を検証するためには、クライアント証明書を発行したCAの証明書をAPI Gatewayに登録しておく必要があります。このCA証明書はAPI Gatewayと同一リージョンのS3から読み込む必要があるので、先程作成したCA証明書を適当なS3バケットにアップします。

$ aws s3 cp RootCA.pem s3://<適当なS3バケット>

クライアント証明書による認証を有効化

続いてAPI Gatewayのカスタムドメインの設定から「Mutual TLS authentication」をONにし、「Trusostore URI」に先程S3にアップしたCAを証明書のパスを指定します。本来はS3のバージョニングを有効にしてバージョンまで指定するのが推奨ですが、今回はお試しなのでバージョン指定は省略しています。

しばらく待つとステータスが「Available」に変わります。

これで準備完了です。

クライアント証明書を使ってAPI Gatewayにアクセスする

API Gatewayの準備ができたので、まずは先程と同様にクライアント証明書無しでAPI Gatewayにアクセスしてみます。

$ curl https://<設定したカスタムドメイン名>
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to <設定したカスタムドメイン名>:443

エラーになりました。

今度はクライアント証明書と秘密鍵の指定を追加してアクセスしてみます。

curl -i --key my_client.key --cert my_client.pem  https://<設定したカスタムドメイン名>
HTTP/2 200
x-amzn-requestid: f6340999-4e61-423d-ae27-919b4e093f5c
x-amz-apigw-id: TMO90GCVNjMFWgA=
x-amzn-trace-id: Root=1-5f67f858-21bfaab0db325647f17eb067;Sampled=0
content-type: application/json
content-length: 3179
date: Mon, 21 Sep 2020 00:48:24 GMT

{"resource": "/", "path": "/", "httpMethod": "GET", "headers": {"accept": "*/*", ...略

無事に200OKでレスポンスが返却されてきました。

なお、今回API GatewayのバックエンドにはeventデータをそのままJSONで返却するだけのLambdaを紐付けています。Lambdaから返却されてきたイベントデータを確認すると、以下のようにrequestContextidentity以下にclientCertという項目が設定されていることが分かります。

{
...略
  "requestContext": {
    ...略
    "identity": {
      "cognitoIdentityPoolId": null,
      "clientCert": {
        "clientCertPem": "<クライアント証明書の内容>",
        "serialNumber": "1",
        "issuerDN": "C=JP,ST=OSAKA,L=HIGOBASHI,O=Classmethod,OU=CX Division,CN=cm-iwata ca",
        "validity": {
          "notAfter": "Aug 28 00:31:30 2120 GMT",
          "notBefore": "Sep 21 00:31:30 2020 GMT"
        },
        "subjectDN": "C=JP,ST=OSAKA,L=HIGOBASHI,O=Classmethod,OU=CX Division,CN=cm-iwata client"
      },
      "cognitoIdentityId": null,
      "principalOrgId": null,
      "cognitoAuthenticationType": null,
      "userArn": null,
      "userAgent": "curl/7.64.1",
      "accountId": null,
      "caller": null,
      "sourceIp": "126.83.27.201",
      "accessKey": null,
      "cognitoAuthenticationProvider": null,
      "user": null
    }
  }
}

subjectDNからクライアントの名前が取得できるので、後続のLambdaで各種データストアの情報と突き合わせて独自のビジネスロジックを実装するといったことも可能になります。

(おまけ)API Gatewayのデフォルトエンドポイントを無効に

カスタムドメインに対してクライアント証明書による認証を設定しましたが、API Gatewayデフォルトのエンドポイント(https://.execute-api.<リージョン>.amazonaws.com/)を利用すると、クライアント証明書無しでもアクセスできる状態になっているので、デフォルトのエンドポイントは無効化しておきましょう。HTTP APIの場合はマネコンから簡単に無効化できるのですが、REST API の場合は現状GUIが提供されていないため、直接AmazonApiGatewayV2のAPIメソッドUpdateApiを実行します。すぐに対応されるとは思いますが、試した時点ではAWS CLIが未対応だったので、Postmanを使ってSIGv4の署名を作りつつ、以下のようなリクエストを発行しました

curl --location --request PATCH 'https://apigateway.ap-northeast-1.amazonaws.com/restapis/<API ID>' \
--header 'X-Amz-Content-Sha256: <ハッシュ値>' \
--header 'X-Amz-Date: 20200921T012547Z' \
--header 'Authorization: AWS4-HMAC-SHA256 Credential=<アクセスキーID>/20200921/ap-northeast-1/apigateway/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=<SIGV4の署名>' \
--header 'Content-Type: application/json' \
--data-raw '{"patchOperations": [{"op": "replace", "path": "/disableExecuteApiEndpoint", "value": "true"}]}'

{"patchOperations": [{"op": "replace", "path": "/disableExecuteApiEndpoint", "value": "true"}]}の部分でデフォルトのエンドポイントを無効化するように指定しています。

実行成功後にしばらく待つとデフォルトのエンドポイントが利用できなくなります。

$ curl  https://<API-ID>.execute-api.ap-northeast-1.amazonaws.com/dev
{"message":"Forbidden"}

まとめ

API Gatewayにクライアント証明書による認証を設定してみました。API Gatewayがクライアント証明書による認証をサポートしたことで、IoT機器からAPIを実行するようなユースケースにおけるアーキテクチャの選択肢が広がったのではないでしょうか?選択肢の1つとしてうまく活用していきたいですね。