Puppeteerでクライアント証明書とパスフレーズが必要なサイトにアクセスする

Puppeteerでクライアント証明書のあるサイトにアクセスする方法をLambdaで実装してます。
2022.07.31

はじめに

CX事業本部の佐藤智樹です。

今回はPuppeteerでクライアント証明書とパスフレーズが必要なサイトにアクセスする方法を紹介します。LambdaとPuppeteerを使って独自にSynthetic Monitoringを実装するため参考になりそうな情報を調べた際、証明書エラーを無視する方式が多かったのですが証明書などを準備して正しくアクセスする方式が日本語だと無さそうだったので記事にしました。どなたかの参考になれば幸いです。

やり方

以下のissueに記載されたコメントのように、Puppeteerのリクエストをインターセプトしよく使うHTTPクライアントを使ってクライアント証明書などを使ったリクエストを実装すれば認証を通すことができます。

上記のリンク先の場合、現在非推奨なrequestクライアントを使用しているので、本記事ではAxiosを使った書き方を紹介します。

Puppeteerのリクエストのインターセプトについては以下のリンクが参考になります。

実際のコード

Lambdaで実装していたためLambdaのコードをベースに紹介します。handlerがエントリーポイントと思ってもらえれば、他の部分はローカルPCやサーバから実行する場合と特に変わらないのです。

import * as fs from "fs";
import * as https from "https";
import * as puppeteer from "puppeteer";
import Axios from "axios";

export const handler = async () => {
  const cert = fs.readFileSync('/path/to/cert.crt'); // 実際はS3から取得
  const key = fs.readFileSync('/path/to/cert.key');  // 同上
  const ca = fs.readFileSync('/path/to/cacert.pem');  // 同上
  const passphrase = "***" // 実際はSecretsManagerに保存して取得

  const httpsAgent = new https.Agent({
    cert,
    key,
    ca,
    passphrase,
  });

  const browser = await getPuppeteer();
  const page = await browser.newPage();

  page.on("request", async (interceptedRequest) => {
    Axios.request({
      url: interceptedRequest.url(),
      method: interceptedRequest.method(),
      headers: interceptedRequest.headers(),
      httpsAgent,
    })
      .then((response) => {
        interceptedRequest.respond({
          status: response.status,
          contentType: response.headers["content-type"],
          headers: response.headers,
          body: response.data,
        });
      })
      .catch((e) => {
        console.log(e);
        return e.response;
      });
  });
  page.setDefaultNavigationTimeout(0);
  await page.goto("REQUEST_URL", { waitUntil: "networkidle0" });

  const result = await page.title();
  console.log(result);

  await page.close();
  await browser.close();
}

const getPuppeteer = async () => {
  return await puppeteer.launch({
    headless: true,
    args: [
      "--no-sandbox",
      "--disable-setuid-sandbox",
      "--disable-dev-shm-usage",
      "--disable-gpu",
      "--no-first-run",
      "--no-zygote",
      "--single-process",
    ],
  });
};

上記のコードを使うとクライアント証明書が必要なサイトに正常にアクセスでき、ページのタイトルが取得できます。後は取得したページの内容に合わせてCloudWatch Metricsに値を投げてCloudWatch Alarmを設定することで正常性の監視や異常時の通知などが実装できます。

おまけ:LambdaでPuppeteerを使う場合の参考記事

Lambda上でPuppeteerを使うのは以下の記事が参考になりました。Lambda Containerで作成して、Dockerfile内でPuppeteerに必要なファイルをインストールしています。

所感

mTLS認証などの場合には、クライアント証明書がないと接続できなかったので実装しました。同じような悩みのある方には参考になるかと思います。

また別の話ですがLambdaについては、いくつかPuppeteerをLambdaで使いやすいようパッケージ化しているものがOSSとして公開されていました。しかしながら、脆弱性対応などを考慮すると関連ライブラリのアップデートなどは可能な限りコントロールしたいため、出来るだけネイティブな状態で使ってます。