Amazon Web Services ブログ

Amazon CloudWatch Synthetics で自己署名証明書を使用して認証を検証する方法

今日のデジタルランドスケープでは、最適なアプリケーションパフォーマンスを確保することが非常に重要であり、Amazon CloudWatch Synthetics はウェブアプリケーションと API のプロアクティブなテストを可能にします。このブログでは、自己署名証明書を利用して監視機能を強化したい場合、エンドポイントの自己署名証明書をサポートするように Canary のソースコードを変更する方法を順を追って説明します。最大の利点は、この方法では認証プロセスを変更する必要がないことです。

Amazon CloudWatch Synthetics Canary をハートビートブループリントとともに使用して、自己署名証明書に依存するアプリケーションをモニタリングする方法を説明します。自己署名証明書とは、(公式機関に) 承認されていない信頼できる第三者認証局(CA)によって発行および署名された証明書です。

さらに、API を監視する他の方法について説明している以前のブログを確認することをお勧めします。Multi-step API monitoring using Amazon CloudWatch Synthetics というタイトルのブログでは、Amazon CloudWatch Synthetics スクリプトを作成して、マルチステップ API 検証を実行し、API の機能性と安定性を確保するための詳細な手順を紹介しています。さらに、2 つ目のブログ How to validate authentication using Amazon CloudWatch Synthetics – Part 2 では、Synthetic ブループリントを更新して、サーバーとクライアントの両方が相互認証 (mTLS) のためにデジタル証明書の提示する必要のあるアプリケーションをモニタリングするための方法を順を追って説明しています。これらのリソースは理解を補完し、包括的な監視戦略を構築するのに役立ちます。

ソリューション概要

このソリューションでは、既存のブループリントに基づいてハートビートモニター Canary を作成し、提供されたブループリントに自己署名証明書を挿入する手順を示します。これにより、ブループリントが提供する機能を拡張できます。

Canary の作成

CloudWatch Synthetics では、すぐに使用できるブループリントスクリプトを利用できます。ただし、証明書で認証するには、コンソールのエディターを使用してコードスニペットを追加する必要があります。

CloudWatch Synthetics がどのように認証を処理するかをシミュレートするために、self-signed.badssl.com ウェブサイトを使用します。独自の HTTP エンドポイントを使用して同じ出力をシミュレートすることもできます。証明書がまだ追加されていないため、最初の呼び出しでは失敗応答が返されます。ただし、このエラーは次の手順で修正されます。

ハートビートブループリントスクリプトに基づいて Canary を作成する手順

  1. CloudWatch コンソールの 「Synthetics」メニューを開きます。
  2. Canary を作成 を選択します。
  3. 設計図のリストから ハートビートのモニタリング を選択します。
  4. 名前 に Canary の名前を入力します。たとえば、「https-selfsigned-test」と入力します。
  5. アプリケーションまたはエンドポイント URL に 「https://self-signed.badssl.com/」 を入力します。
  6. その他の設定はすべてデフォルトのままにしてください。必要な権限を持つ IAM ロールが自動的に作成され、さらに「cw-syn-results-accountID-region」 という名前のバケットが作成されます。バケットがアカウントにすでに存在する場合は、「cw-syn-results」 という名前を検索するか、独自の名前を作成できます。詳細については、 Canary を作成する を参照してください。
  7. 「Canary を作成」を選択します。Canary が作成されると、図 1 に示すように、Canary のリストに表示されます。Canary のハートビートモニターブループリントの使用方法については、Amazon CloudWatch ユーザーガイド の ハートビートのモニターリング を参照してください。
Canaries page of the CloudWatch console

図 1. CloudWatch コンソールの Canary ページ

レポートの確認

Canary レポートには、すべてのステップと結果が表示されます。この場合、Canary は図 2 に示すように ERR_CERT_AUTHORITY_INVALID エラーを返しました。エンドポイントは信頼できる CA によって発行されていない証明書を使用しており、Canary は信頼された認証局に対して自己署名証明書の信頼性を検証できないため、このエラーは予想されるものです。

http-steps-test report failing with the error ERR_CERT_AUTHORITY_INVALID

図 2. HTTP-steps-test レポートが ERR_CERT_AUTHORITY_INVALID というエラーで失敗

証明書の詳細をダウンロード

自己署名証明書に関する検証の問題に対処するために、badsslのWebサイトから必要な証明書をダウンロードできます。このダウンロードされた証明書は信頼を確立する目的で使用され、 self-signed.badssl.com エンドポイントに対して行われた API リクエストを認証するために Canary によって使用されます。以下のステップは、AWS CloudShell 上で CLI コマンドを使用して、これを実現する方法を示していますが、これらの証明書はブラウザを使用して badssl ウェブサイトから手動でダウンロードすることもできます。

Note: 発行元が信頼できることが確実でない限り、自己署名証明書はお勧めしません。

badssl.com からパブリック証明書をダウンロード

この手順では、AWS リソースの安全な管理、探索、操作を容易にするブラウザベースのシェルである AWS CloudShell を使用します。以下のスクリプトを実行するには CloudShell をお勧めします。ただし、同じ出力には独自のコマンドラインを使用することもできます。

self-signed.badssl.com から.pem ファイルをダウンロードするには、以下の手順に従います。

  1. CloudShell コンソールを開きます。
  2. 環境が作成されるのを待ちます。
  3. 以下のスクリプトをコピーして、AWS CloudShell によって作成された bash コンソールに貼り付けます。
# Install openssl if you don't have that installed
# Ubuntu users -> sudo apt-get install openssl
sudo yum install openssl -y
 
# Download the self-signed.badssl.com certificate 
openssl s_client -showcerts -connect self-signed.badssl.com:443 -servername self-signed.badssl.com </dev/null | sed -n -e '/-.BEGIN/,/-.END/ p' | awk '{printf "%s%s", (NR>1 ? "\\n" : ""), $0} END {print ""}' > self-signed.pem

# Get the certificate content to be pasted on the canary code
cat self-signed.pem
  1. 「貼り付け」を選択します。
CloudShell popup to paste multi-line text

図 3. 複数行のテキストを貼り付けるための CloudShell ポップアップ

  1. 上記のスクリプトは証明書を含むテキストを生成します。次のステップで使用するため、 —–BEGIN CERTIFICATE—– で始まり —–END CERTIFICATE—– で終わる内容をコピーします。

Canary スクリプトを編集して証明書を追加

証明書をインポートするには、作成した Canary にアクセスし、先ほどコピーした証明書の出力を含む変数を追加する必要があります。

コードを編集するには、次の手順に従います。

  1. CloudWatch コンソールSynthetics メニューを開きます。
  2. 作成した Canary を選択してください。たとえば、https-selfsigned-test です。
  3. アクション を選択し、編集 を選択します。
  4. スクリプトエディタのボックスを使用して、スクリプトの上部で https パッケージをインポートします。このパッケージにより、スクリプトは証明書に利用するようにプロトコルの設定を調整できます。
const https = require('https');
  1. ‘page.goto()’ 関数を使用してエンドポイントにアクセスする前に、以下のコードブロックを挿入してください。ブループリントの場合、これは 非同期関数 loadBlueprint の ‘const sanitizedUrl’ 行と ‘const response = await page.goto...’ 行の間にあります。
await page.setRequestInterception(true);

const cert = '<YOUR_CERTIFICATE_HERE>'

log.info("Injecting request with self-signed certificate.");

page.on("request", (interceptedRequest) => {
  const options = {
    method: interceptedRequest.method(),
    headers: interceptedRequest.headers(),
    body: interceptedRequest.postData(),
    ca: cert,
    cert: cert,
  };
  const request = https
    .request(interceptedRequest.url(), options, function (response) {
      response.on("data", function (data) {
        interceptedRequest.respond({
          status: response.statusCode,
          contentType: response.headers["content-type"],
          headers: response.headers,
          body: data,
        });
      });
    })
    .on("error", function (err) {
      console.error("Unable to call %s", options.uri, err);
      return interceptedRequest.abort("connectionrefused");
    });
  request.end();
});

Note: このコードは、Canary からエンドポイントへのリクエストをインターセプトし、アップロードした証明書を信頼できる認証局として含めるようにリクエストオプションを変更します。

例えば、コードの全文は以下のようになります。

const { URL } = require('url');
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = synthetics.getConfiguration();
const syntheticsLogHelper = require('SyntheticsLogHelper');
const https = require('https');

const loadBlueprint = async function () {

    const urls = ['https://self-signed.badssl.com/'];

    // Set screenshot option
    const takeScreenshot = true;

    /* Disabling default step screen shots taken during Synthetics.executeStep() calls
     * Step will be used to publish metrics on time taken to load dom content but
     * Screenshots will be taken outside the executeStep to allow for page to completely load with domcontentloaded
     * You can change it to load, networkidle0, networkidle2 depending on what works best for you.
     */
    syntheticsConfiguration.disableStepScreenshots();
    syntheticsConfiguration.setConfig({
       continueOnStepFailure: true,
       includeRequestHeaders: true, // Enable if headers should be displayed in HAR
       includeResponseHeaders: true, // Enable if headers should be displayed in HAR
       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

    });

    let page = await synthetics.getPage();
    await certRequestIntercept(page)

    for (const url of urls) {
        await loadUrl(page, url, takeScreenshot);
    }
};

const certRequestIntercept = async function(page) {
    await page.setRequestInterception(true);
    const cert = '<YOUR_CERTIFICATE_HERE>'

    log.info("Injecting request with self-signed certificate.");

    page.on("request", (interceptedRequest) => {
        const options = {
            method: interceptedRequest.method(),
            headers: interceptedRequest.headers(),
            body: interceptedRequest.postData(),
            ca: cert,
            cert: cert,
        };
        const request = https.request(interceptedRequest.url(), options, function (response) {
            response.on("data", function (data) {
                interceptedRequest.respond({
                status: response.statusCode,
                contentType: response.headers["content-type"],
                headers: response.headers,
                body: data,
                });
            });
        }).on("error", function (err) {
            console.error("Unable to call %s", options.uri, err);
            return interceptedRequest.abort("connectionrefused");
        });
        request.end();
    });
}

// Reset the page in-between
const resetPage = async function(page) {
    try {
        await page.goto('about:blank',{waitUntil: ['load', 'networkidle0'], timeout: 30000} );
    } catch (e) {
        synthetics.addExecutionError('Unable to open a blank page. ', e);
    }
}

const loadUrl = async function (page, url, takeScreenshot) {
    let stepName = null;
    let domcontentloaded = false;

    try {
        stepName = new URL(url).hostname;
    } catch (e) {
        const errorString = `Error parsing url: ${url}. ${e}`;
        log.error(errorString);
        /* If we fail to parse the URL, don't emit a metric with a stepName based on it.
           It may not be a legal CloudWatch metric dimension name and we may not have an alarms
           setup on the malformed URL stepName.  Instead, fail this step which will
           show up in the logs and will fail the overall canary and alarm on the overall canary
           success rate.
        */
        throw e;
    }

    await synthetics.executeStep(stepName, async function () {
        const sanitizedUrl = syntheticsLogHelper.getSanitizedUrl(url);

        /* You can customize the wait condition here. For instance, using 'networkidle2' or 'networkidle0' to load page completely.
           networkidle0: Navigation is successful when the page has had no network requests for half a second. This might never happen if page is constantly loading multiple resources.
           networkidle2: Navigation is successful when the page has no more then 2 network requests for half a second.
           domcontentloaded: It's fired as soon as the page DOM has been loaded, without waiting for resources to finish loading. If needed add explicit wait with await new Promise(r => setTimeout(r, milliseconds))
        */
        const response = await page.goto(url, { waitUntil: ['domcontentloaded'], timeout: 30000});
        if (response) {
            domcontentloaded = true;
            const status = response.status();
            const statusText = response.statusText();

            logResponseString = `Response from url: ${sanitizedUrl}  Status: ${status}  Status Text: ${statusText}`;

            //If the response status code is not a 2xx success code
            if (response.status() < 200 || response.status() > 299) {
                throw new Error(`Failed to load url: ${sanitizedUrl} ${response.status()} ${response.statusText()}`);
            }
        } else {
            const logNoResponseString = `No response returned for url: ${sanitizedUrl}`;
            log.error(logNoResponseString);
            throw new Error(logNoResponseString);
        }
    });

    // Wait for 15 seconds to let page load fully before taking screenshot.
    if (domcontentloaded && takeScreenshot) {
        await new Promise(r => setTimeout(r, 15000));
        await synthetics.takeScreenshot(stepName, 'loaded');
        await resetPage(page);
    }
};

const urls = [];

exports.handler = async () => {
    return await loadBlueprint();
};
  1. 先ほど貼り付けたコードの<YOUR_CERTIFICATE_HERE> を置き換えて、証明書の内容を追加します。
const cert = '<YOUR_CERTIFICATE_HERE>'
  1. 保存 を選択します。

証明書のハードコーディングはベストプラクティスではありませんが、このブログでは手順を効率化するためにこのアプローチを採用しました。加えて、今回の証明書は公開されているため、リスクは低くなります。ベストプラクティスに従い、環境へデプロイする際のリスクを軽減には、How to validate authentication using Amazon CloudWatch Synthetics – Part 2 の説明のように、AWS Secrets Manager を使用することで、シークレットを安全に保存できます。

コードを変更したら、保存して Canary が再び実行されるのを待ちます。Canary の実行 が 成功 し、ステップのタブでリクエストステータスが 成功 と表示されるはずです。

https-selfsigned-test showing the status as Passed

図 4: https-selfsigne-test のステータスが「成功」と表示される

Synthetics の使用方法にすでに慣れているユーザー向けに、証明書を含む Canary スクリプトを ZIP ファイルにバンドルできる高度なオプションが用意されています。このアプローチについては、Node.js Canary スクリプトの記述Python Canary スクリプトの記述 というドキュメントで説明されており、シームレスな実装のための包括的なガイダンスが提供されています。

クリーンアップ

Canary を削除すると元に戻すことはできず、関連するデータと設定はすべて失われることに注意してください。削除を続行する前に、必要なバックアップをすべて作成するか、関連するデータをすべて抽出したことを確認してください。

CloudWatch Synthetics Canary を削除するには、以下の手順に従ってください。

  1. CloudWatch コンソールSynthetics メニューを開きます。
  2. 上記で作成した Canary を選択してください。たとえば、https-selfsigned-test です。
  3. アクション を選択し、中止 を選択します。
  4. Canary の状態が 停止 に変わるまで待ってください。
  5. もう一度 アクション を選択し、 削除 を選択します。
  6. Canary が使用するロールロールポリシーなど、削除するリソースを選択します。
  7. Canary によって作成されたリソースを削除するには、削除 を選択します。
  8. このワークショップで作成したリソースを削除することを確認するには、フィールドに Delete というフレーズを入力します。
  9. 確認 を選択します。

コンソール上で、選択したリソースが削除され、Canary が正常に削除されたことが表示されます。

Synthetics page showing that the canary was deleted

図 5. Canary が削除されたことを示す Synthetics ページ

結論

このブログでは、CloudWatch Synthetics で自己署名証明書を利用する方法について説明しました。これにより、証明書が信頼できるサードパーティ CA によって署名されていなくても、ブループリントの機能を拡張して API に接続できるようになりました。これらの知見を武器に、自信を持って自己署名証明書を自分のプロジェクトに実装できます。

この機能やその他のすべての機能の使用方法の詳細については、CloudWatch Synthetics のドキュメントをご覧ください。また、Synthetics の AWS Command Line Interface (CLI) ドキュメントは こちら にあります。

著者について

Mario Jorge Salheb Leitao

Mario Leitao は、シドニーを拠点とする AWS Managed Services (AMS) チームの Senior Cloud Architect です。技術ガイダンスを提供し、お客様が AWS で成功できるように、費用対効果が高く効率的なソリューションを設計および構築しています。Cloud Architect として働く前は、アイルランドのダブリンで Network Development Engineer として働いていました。

Matheus Canela

Matheus は、Amazon Web Services の Solutions Architect として、デジタルネイティブ企業にテクノロジープラットフォームの変革について助言し、ベストプラクティスに従ってあらゆるレベルのエンジニアが目標を達成できるよう支援しています。AWS に入社する前、Matheus はシドニーを拠点とする Premier Consulting Partner である CMD Solutions で Senior DevOps Consultant を務めていました。Matheus は開発者およびセキュリティスペシャリストとしても働いてきました。コミュニティを支援することが彼のDNAであるため、.NET Meetups を開催し、IT.BR コミュニティを支援して、ブラジルからオーストラリアに移住するエンジニアを支援しています。

Edgar Yu

Edgar は AWS の CloudWatch Synthetics チームの Software Development Engineer です。彼の現在の焦点は、顧客がより多くのパフォーマンスデータを収集し、ウェブアプリケーションをより適切に監視できるようにするソフトウェアの構築と改善にあります。AWS 以外では、彼は食事を楽しんでいます。

翻訳はテクニカルアカウントマネージャーの日平が担当しました。原文は こちら です。