CloudWatch Synthetics Canaries でクライアント証明書が必要なエンドポイントの監視を行う

CloudWatch Synthetics Canaries を使って、クライアント証明書が必要なエンドポイントの監視を設定してみました。 AWS 公式ブログのチュートリアル手順に沿って設定すると、20〜30分程度で設定が完了しました!
2022.03.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

おはこんばんちは。オペレーション部のもっさんです。

今回は、Amazon CloudWatch Synthetics の Canary を用いて、実行にクライアント証明書 を必要とする API 監視を設定します。

Amazon CloudWatch Synthetics とは?

Amazon CloudWatch Synthetics (以下、CloudWatch Synthetics )は Web ページや API に対する模擬モニタリング(監視)を設定できるマネージドサービスです。
Canary と呼ばれるスクリプトを作成・スケジュール実行することで、 エンドユーザーによる WEB サイトのアクセスや API 実行をシミュレーションし、結果をモニタリングします。このモニタリングにより、エンドユーザーからのアクセスと同じ状況で、トラブルが発生していないかを確認することができます。

CloudWatch Synthetics の詳細は、次のブログ記事に記載されています。ご参照ください。

設定手順

AWS 公式のブログで紹介されているチュートリアル手順に沿って設定します。こちらの記事では、設定手順を試すための検証用 アプリケーションURLが準備されているため、監視対象のアプリケーションを作成しなくても API Canary の動作を手軽に試すことができます。

(未作成の場合) Canary の作成

エンドポイント 監視のための Canary 設定がない場合は、まず Canary の作成を行います。すでに適切な Canary を作成している場合、この手順はスキップします。

AWS マネジメントコンソールで CloudWatch を開き、メニューの左ペインから Synthetics を選択します。
[Canary の作成]をクリックします。

次の画面では、 Canary の作成方法を選択できます。[設計図を使用する]を選択します。このオプションを使用することで、 Canary を実行するための テンプレートスクリプトを使用でき、自前でスクリプトを準備するよりも簡単に設定が可能です。
ただし、クライアント証明書による認証を使用する際は、少しだけテンプレートスクリプトにコードを追記する必要があります。追記するコードの詳細については「テンプレートの更新」で後述します。

「設計図」の項目で、[API Canary]を選択します。

「Canary ビルダー」の「名前」項目に、任意のステップ名を入力します。今回は手順の例にならって、[http-steps-test]とします。

「HTTP リクエスト」項目内の、[HTTP リクエストを追加]をクリックします。「HTTP リクエストの詳細」ポップアップが開きます。

「方法」のプルダウンメニューから[GET]を選択します。
「テストするアプリケーションまたはエンドポイントURL」で、監視対象の URL を入力します。今回は、手順に掲載されている検証用 URL を入力します。

「ステップ名」の項目は自動で文字列が入力されますが、テキストボックスに入力し直すことで、任意の文字列を設定することが可能です。このステップ名は、同じ Canary で複数のエンドポイントを監視の対象にしている等の場合、ステップを特定するために役立ちます。
[保存]をクリックすると、ポップアップがクローズします。先程入力したエンドポイントとステップ名が、「HTTP リクエスト」の一覧に追加されていることを確認します。

「スクリプトエディタ」の項目では、スクリプトが自動生成されています。Node.js または Python のランタイムバージョンを選択可能ですが、今回はデフォルトのまま設定します。2022/03 現在でのデフォルトのランタイムバージョンは、syn-nodejs-pupeteer-3.4になっています。
「環境変数」は後ほどスクリプトで設定するため、現段階では入力しないままにしておきます。

今回のチュートリアル進行のために Canary を作成している場合、以降の設定項目はデフォルトのままで問題ありません。コンソールの設定画面を一番下までスクロールし、[Canaryの作成]をクリックします。

チュートリアルではスキップする各種設定について

この項目は、チュートリアル手順では設定をスキップする各種設定について、簡単に説明しています。必要要件に応じて設定してください。
ただし、前述のとおり、チュートリアルを進めるだけであればデフォルト設定のままで進められます。こちらの各種設定についての記載は読み飛ばしても問題ありません。
スキップする場合は、[証明書と鍵のインポート]項目まで進んでください。

スケジュール
「スケジュール」では、 Canary の実行頻度を設定できます。今回はデフォルトの[継続的に実行] とし、5分間隔での Canary 実行をスケジュールしておきます。

スケジュールは次の3つから選択できます。 Canary の作成後に設定を変更することも可能です。

  • 継続的に実行
    • 指定した間隔ごとに Canary を実行します。指定できる頻度は1〜60分の範囲内です。
  • CRON 式
    • cron 式で実行するタイミングをスケジュールします。毎日 13 時に実行するなど、特定の曜日や時間に実行をスケジュールする際に便利です。
  • 1回実行
    • Canary の作成時にのみ、実行を行います。

データの保持期間の選択
「データ保持」の項目では、 Canary レポートなどのデータをどのくらいの期間保持するかを指定できます。この項目は、障害発生時のデータと成功時(正常時)のデータ、それぞれに対して個別に期間設定が可能です。
デフォルトの設定では、障害発生時のデータ、成功時のデータの両方とも[31日(1ヶ月)]になっています。指定できる範囲は1日〜455日です。

データストレージ
Canary 実行によるアーティファクト(実行結果のレポートなどのデータ)は S3 に保存されます。この項目では、データの保存に関する以下のを設定を行います。

  • アーティファクトの保存先 S3 バケットの指定
    • アーティファクトの保存先 S3 バケットを URI 形式で指定することができます。
    • 空欄(デフォルト)の場合は、この Canary のアーティファクトの保存用に、新規 S3 バケットが作成されます。
  • 暗号化方法の選択
    • デフォルトでは、 Canary アーティファクトは AWS マネージドキーを用いて暗号化されます。
    • 設定によって、以下の暗号化方式を選択可能です。
      • SSE-S3
      • SSE-KMS

アーティファクトの暗号化についての詳細は、次の AWS ドキュメントで詳細を確認できます。

アクセス許可
Canary の実行に使用する IAM ロールを指定します。デフォルトでは、「新しいロールを作成」が指定されています。こちらを選択した場合、CloudWatch Synthetics が Canary 実行に必要な権限をもつ IAM ロールを自動で作成します。
既存の IAM ロールを指定することも可能です。

CloudWatchアラーム
この項目はオプションであり、設定しなくても Canary の作成は可能です。
Canary で監視している項目に、アラームを設定することができます。Amazon SNS と連携して、 SNS トピックにアラーム通知を設定することもできます。
アラームの通知先 SNS は、新しく SNS トピックを作成し指定することも、既存の SNS トピックを指定することも可能です。ただし、既存の SNS トピックを指定する場合、 SNS トピックは Canary を作成しているアカウントと同じアカウント内に存在している必要があります。

VPC設定
CloudWatch Syntetics は内部的に Lambda で実行されます。そのため、監視対象のエンドポイント(アプリケーション)が VPC 内にあり、パブリックインターネットからのアクセスを許可していない場合は Lambda VPC 接続を設定する必要があります。この項目では、CloudwWatch Syntetics で作成する Lambda が Lambda VPC 接続を行うための項目を設定できます。
なお、CloudWatch Syntetics では CloudWatch メトリクスと S3 も使用します。監視対象のアプリケーションがパブリックインターネットからのアクセスを許可していない場合は、 CloudWatch メトリクスと S3 アクセスのために、 VPC エンドポイントを別途準備しておく必要があります。

タグ
この項目はオプションであり、設定しなくても Canary の作成は可能です。
Canary に任意のタグを設定することができます。

アクティブトレース
この Canary と AWS X-Ray を連携するかを選択できます。AWS X-Ray は、アプリケーションの分析およびデバックに役立つサービスです。
AWS X-Ray についての詳細は、次のドキュメントを参照してください。

証明書と鍵のインポート(スクリプトを使用)

Synthetics Canaries のページでは、作成済みの Canary が一覧表示されます。ここで、クライアント証明書が必要なアプリケーションを監視する Canary を選択します。

この段階では、Canary は「失敗」となっています。Canary 名を選択して、 HTTP リクエストの詳細を確認すると、ステータスコード 400 を記録して失敗していることがわかります。これは、クライアント証明書を設定できていないため、Canary によるアプリケーションへのアクセスが拒否されていることを示しています。証明書で認証できないアクセスを拒否しているため、想定通りの挙動といえます。
ここから、Canary に対してクライアント証明書と鍵のインポートを行います。

証明書と鍵のインポートはマネジメントコンソールを介して手動で行うこともできますが、今回はチュートリアルで準備されたスクリプトにより、プログラマブルに実行します。
今回は、スクリプトの実行に CloudShell を使用します。

# 環境変数の設定
# AWS_REGION の値は、Canaryを作成したリージョンコードを指定してください
# SYN_NAME の値は、対象のCanaryの名前を指定してください
export AWS_REGION=ap-northeast-1
export SYN_NAME="http-steps-test"
export SECRETBADSSLKEYNAME=badsslkey
export SECRETBADSSLCERTNAME=badsslcert
export THRESOLDCERTDAYEXP=5

# AWS CLI をバージョン2の最新版にアップデートする
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update

# クライアントに openssl をインストールする
# すでに openssl がインストールされている場合、このコマンドは省略可能
sudo yum install openssl -y

# 証明書と鍵を保存するためのディレクトリを作成し、カレントディレクトリを移動する
mkdir badsslcert
cd badsslcert
# client.badssl.com からpemファイルをダウンロードする
# client.badssl.com 以外の別のエンドポイントの証明書をダウンロードする場合は、wgetコマンドの後のURLは置き換えが必要
wget https://badssl.com/certs/badssl.com-client.pem

# 証明書と鍵のエクスポート
# client.badssl.com以外のpemファイルを使用する場合、-passin オプションで指定しているパスワードは当該pemファイルに対応したものに置き換えが必要
openssl rsa -in badssl.com-client.pem -out badssl.com-client.key -passin pass:badssl.com
openssl x509 -in badssl.com-client.pem -trustout -out badssl.com-client.cert

# Secrets Manager にシークレットを作成する
badsslkeyarn=$(aws secretsmanager create-secret --name $SECRETBADSSLKEYNAME --secret-string file://badssl.com-client.key --output text --query ARN)
badsslcertarn=$(aws secretsmanager create-secret --name $SECRETBADSSLCERTNAME --secret-string file://badssl.com-client.cert --output text --query ARN)

# Canary で使用するための IAM ロール名を取得する
synrole=$(aws synthetics get-canary --name $SYN_NAME --query Canary.ExecutionRoleArn --output text)
synrole=$(echo $synrole | cut -d "/" -f3)

# Canary が Secrets Manager のシークレットにアクセスするためのインラインポリシーを作成する
# ポリシーはこの Canary で使用するシークレットへのアクセスのみを許可するものになっている
echo "{
    \""Version\"": \""2012-10-17\"",
    \""Statement\"": [
        {
            \""Effect\"": \""Allow\"",
            \""Action\"": \""secretsmanager:GetSecretValue\"",
            \""Resource\"": [
                \""$badsslkeyarn\"",
                \""$badsslcertarn\""
             ]
        }
    ]
}" >> inline-policy.json

# IAM ロールの作成とインラインポリシーのアタッチ
aws iam put-role-policy --role-name $synrole --policy-name "allow-get-secrets-certs" --policy-document file://inline-policy.json

# 証明書のフィンガープリントを読み込む
badsslsha256=$(openssl s_client -connect client.badssl.com:443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -noout -in /dev/stdin)
badsslsha256=$(echo $badsslsha256 | cut -d "=" -f2)

# 現在ターミナルで使用している環境変数を Canary の環境変数に設定する
aws synthetics update-canary --name $SYN_NAME --run-config 'EnvironmentVariables={BADSSLKEY='$SECRETBADSSLKEYNAME',BADSSLCERT='$SECRETBADSSLCERTNAME',CERTSHA256='$badsslsha256',THRESOLDCERTDAYEXP='$THRESOLDCERTDAYEXP'}'

# ダウンロードした証明書と鍵をディレクトリごと削除する
# すでにシークレットはSecrets Manager に保存してあるため、ローカル作業環境からは削除しても問題ない
cd ..
rm -r badsslcert/

# スクリプト実行完了
echo "Script finished"

これで、クライアント証明書と鍵の情報を Secrets Manager に保存し、 Canary が使用するロールに適切な権限を付与することができました。

テンプレートの更新

Canary がシークレットを読み込み、エンドポイントの検証に使用するには、 Canary の「スクリプトエディタ」からスクリプトを更新する必要があります。
再度、Cloudwatch Synthetics のコンソールに戻り、作成した Canary を選択します。[アクション] - [編集] をクリックし、 Canary の編集画面に移動します。
「スクリプトエディタ」内のスクリプトを、以下のコードに書き換えます。
なお、こちらはランタイムsyn-nodejs-pupeteer-3.4を使用している場合のコードです。別のランタイムを使用している場合は、必要に応じて書き換えてください。

var synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = synthetics.getConfiguration();

// Load the AWS SDK and the Secrets Manager client.
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

// Connect to the Secrets Manager the load the Key and Cert
// These are the secrets created previously
// The code is dynamic and load the secrets name via environment vars
const getKeyCert = async () => {
  // Add the line below to load the key and cert from the function getKeyCert()
    
    var params = {
        SecretId: process.env.BADSSLKEY
    };
    const key = await secretsManager.getSecretValue(params).promise();
    
    var params = {
        SecretId: process.env.BADSSLCERT
    };
    const cert = await secretsManager.getSecretValue(params).promise();

    // returning Key and Cert
    return [ key.SecretString, cert.SecretString ]
}

const apiCanaryBlueprint = async function () {
    const [ key, cert ] = await getKeyCert();
    
    syntheticsConfiguration.setConfig({
        restrictedHeaders: [], // Value of these headers will be redacted from logs and reports
        restrictedUrlParameters: [] // Values of these url parameters will be redacted from logs and reports
    });
    
    // Handle validation for positive scenario
    const validateSuccessful = async function(res) {
        return new Promise((resolve, reject) => {
            if (res.statusCode < 200 || res.statusCode > 299) {
                throw res.statusCode + ' ' + res.statusMessage;
            }
     
            let responseBody = '';
            res.on('data', (d) => {
                responseBody += d;
            });
     
            res.on('end', () => {
                // Add validation on 'responseBody' here if required.
                resolve();
            });
        });
    };
    

    // Set request option for test client.badssl.com
    let requestOptionsStep1 = {
        hostname: 'client.badssl.com',
        method: 'GET',
        path: '/',
        port: '443',
        protocol: 'https:',
        body: "",
        headers: {},
        key: key, //client.badssl.com key from Secrets Manager
        cert: cert //client.badssl.com cert from Secrets Manager
    };
    requestOptionsStep1['headers']['User-Agent'] = [synthetics.getCanaryUserAgentString(), requestOptionsStep1['headers']['User-Agent']].join(' ');

    // Set step config option for test client.badssl.com
   let stepConfig1 = {
        includeRequestHeaders: false,
        includeResponseHeaders: false,
        includeRequestBody: false,
        includeResponseBody: false,
        continueOnHttpStepFailure: true
    };

    await synthetics.executeHttpStep('test client.badssl.com', requestOptionsStep1, validateSuccessful, stepConfig1);

    
};

exports.handler = async () => {
    return await apiCanaryBlueprint();
};

一番下までスクロールし、[保存]をクリックします。

確認

スクリプトの更新から数分待つと、 Canary の実行ステータスが [成功] に切り替わっていくのが確認できます。
こちらで設定は完了です。お疲れ様でした。

参考

同一の Canary で複数のエンドポイントを監視する場合の例は、次の記事で紹介されています。