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 を作成する手順
- CloudWatch コンソールの 「Synthetics」メニューを開きます。
- Canary を作成 を選択します。
- 設計図のリストから ハートビートのモニタリング を選択します。
- 名前 に Canary の名前を入力します。たとえば、「https-selfsigned-test」と入力します。
- アプリケーションまたはエンドポイント URL に 「https://self-signed.badssl.com/」 を入力します。
- その他の設定はすべてデフォルトのままにしてください。必要な権限を持つ IAM ロールが自動的に作成され、さらに「cw-syn-results-accountID-region」 という名前のバケットが作成されます。バケットがアカウントにすでに存在する場合は、「cw-syn-results」 という名前を検索するか、独自の名前を作成できます。詳細については、 Canary を作成する を参照してください。
- 「Canary を作成」を選択します。Canary が作成されると、図 1 に示すように、Canary のリストに表示されます。Canary のハートビートモニターブループリントの使用方法については、Amazon CloudWatch ユーザーガイド の ハートビートのモニターリング を参照してください。
レポートの確認
Canary レポートには、すべてのステップと結果が表示されます。この場合、Canary は図 2 に示すように ERR_CERT_AUTHORITY_INVALID エラーを返しました。エンドポイントは信頼できる CA によって発行されていない証明書を使用しており、Canary は信頼された認証局に対して自己署名証明書の信頼性を検証できないため、このエラーは予想されるものです。
証明書の詳細をダウンロード
自己署名証明書に関する検証の問題に対処するために、badsslのWebサイトから必要な証明書をダウンロードできます。このダウンロードされた証明書は信頼を確立する目的で使用され、 self-signed.badssl.com エンドポイントに対して行われた API リクエストを認証するために Canary によって使用されます。以下のステップは、AWS CloudShell 上で CLI コマンドを使用して、これを実現する方法を示していますが、これらの証明書はブラウザを使用して badssl ウェブサイトから手動でダウンロードすることもできます。
Note: 発行元が信頼できることが確実でない限り、自己署名証明書はお勧めしません。
badssl.com からパブリック証明書をダウンロード
この手順では、AWS リソースの安全な管理、探索、操作を容易にするブラウザベースのシェルである AWS CloudShell を使用します。以下のスクリプトを実行するには CloudShell をお勧めします。ただし、同じ出力には独自のコマンドラインを使用することもできます。
self-signed.badssl.com から.pem ファイルをダウンロードするには、以下の手順に従います。
- CloudShell コンソールを開きます。
- 環境が作成されるのを待ちます。
- 以下のスクリプトをコピーして、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
- 「貼り付け」を選択します。
- 上記のスクリプトは証明書を含むテキストを生成します。次のステップで使用するため、 —–BEGIN CERTIFICATE—– で始まり —–END CERTIFICATE—– で終わる内容をコピーします。
Canary スクリプトを編集して証明書を追加
証明書をインポートするには、作成した Canary にアクセスし、先ほどコピーした証明書の出力を含む変数を追加する必要があります。
コードを編集するには、次の手順に従います。
- CloudWatch コンソールの Synthetics メニューを開きます。
- 作成した Canary を選択してください。たとえば、https-selfsigned-test です。
- アクション を選択し、編集 を選択します。
- スクリプトエディタのボックスを使用して、スクリプトの上部で https パッケージをインポートします。このパッケージにより、スクリプトは証明書に利用するようにプロトコルの設定を調整できます。
const https = require('https');
‘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();
};
- 先ほど貼り付けたコードの<YOUR_CERTIFICATE_HERE> を置き換えて、証明書の内容を追加します。
const cert = '<YOUR_CERTIFICATE_HERE>'
- 保存 を選択します。
証明書のハードコーディングはベストプラクティスではありませんが、このブログでは手順を効率化するためにこのアプローチを採用しました。加えて、今回の証明書は公開されているため、リスクは低くなります。ベストプラクティスに従い、環境へデプロイする際のリスクを軽減には、How to validate authentication using Amazon CloudWatch Synthetics – Part 2 の説明のように、AWS Secrets Manager を使用することで、シークレットを安全に保存できます。
コードを変更したら、保存して Canary が再び実行されるのを待ちます。Canary の実行 が 成功 し、ステップのタブでリクエストステータスが 成功 と表示されるはずです。
Synthetics の使用方法にすでに慣れているユーザー向けに、証明書を含む Canary スクリプトを ZIP ファイルにバンドルできる高度なオプションが用意されています。このアプローチについては、Node.js Canary スクリプトの記述 と Python Canary スクリプトの記述 というドキュメントで説明されており、シームレスな実装のための包括的なガイダンスが提供されています。
クリーンアップ
Canary を削除すると元に戻すことはできず、関連するデータと設定はすべて失われることに注意してください。削除を続行する前に、必要なバックアップをすべて作成するか、関連するデータをすべて抽出したことを確認してください。
CloudWatch Synthetics Canary を削除するには、以下の手順に従ってください。
- CloudWatch コンソールの Synthetics メニューを開きます。
- 上記で作成した Canary を選択してください。たとえば、https-selfsigned-test です。
- アクション を選択し、中止 を選択します。
- Canary の状態が 停止 に変わるまで待ってください。
- もう一度 アクション を選択し、 削除 を選択します。
- Canary が使用するロールとロールポリシーなど、削除するリソースを選択します。
- Canary によって作成されたリソースを削除するには、削除 を選択します。
- このワークショップで作成したリソースを削除することを確認するには、フィールドに Delete というフレーズを入力します。
- 確認 を選択します。
コンソール上で、選択したリソースが削除され、Canary が正常に削除されたことが表示されます。
結論
このブログでは、CloudWatch Synthetics で自己署名証明書を利用する方法について説明しました。これにより、証明書が信頼できるサードパーティ CA によって署名されていなくても、ブループリントの機能を拡張して API に接続できるようになりました。これらの知見を武器に、自信を持って自己署名証明書を自分のプロジェクトに実装できます。
この機能やその他のすべての機能の使用方法の詳細については、CloudWatch Synthetics のドキュメントをご覧ください。また、Synthetics の AWS Command Line Interface (CLI) ドキュメントは こちら にあります。
著者について
翻訳はテクニカルアカウントマネージャーの日平が担当しました。原文は こちら です。