JPRSサーバー証明書のACME自動更新とACM連携をLambdaで実現する
当社はJPRS(株式会社日本レジストリサービス)様のサーバー証明書を指定事業者として販売しております。現在までは有効期限が1年のものを手動で発行して、お客様のサーバーに設置などを行ってきました。
今現在は最大13ヶ月の有効期限の証明書が認められていることになります。
しかし、CA/Browser Forumや各ブラウザベンダーがこの有効期限を縮める方向で動いているとのことで、将来的にはポリシー改訂により、13ヶ月の有効期限が47日間になるとのことです。
47日ごとに発行はとても人の手では行えません。そこでJPRS様からもACME(※)による自動更新への切り替えを提案頂いております。
※ACME(アクミー)はAutomatic Certificate Management Environment(自動証明書管理環境)に由来する、証明書の管理を自動化するためのプロトコルです。
ACMEを使った自動更新のプログラムとしては有名なものはcertbotなどがあります。無料証明書のLet's Encryptの発行などで利用された経験がある人も多いと思います。JPRS様のサーバー証明書もこのcertbotによる自動更新が行えます。
EC2などのWEBサーバー(Apache)にcertbotを入れることによって、証明書の取得から自動的な更新まで構成することが可能です。
課題:ACMを利用するCloudFront/ALB構成
当社では1つ困ったことがありました。AWSクラウドのCloudFrontディストリビューションや、ALBなどのロードバランサーにアタッチしているACMにJPRS様のサーバー証明書を入れているパターンがあるのです。
この構成の場合、オリジンがEC2だったとしても、CloudFrontやALBでHTTPSを終端し、EC2インスタンス内には証明書を持っていないケースが多いです。
また、CloudFrontでHTMLによる静的なWEBサイトやReact+ViteなどによるSPAの場合は、バックエンドをS3バケットとしており、EC2自体を使用していない場合もあります。
これらの場合は、ACMに証明書をインポートすることで、CloudFrontやALBで持ち込みの証明書を使用することが実現しています。
つまりCertbotを使って、そのままではACMに証明書を発行したりインポートしたりすることが出来ません。世の中には様々なツールを作られている方がいるので、もしかするとプラグインなどを使えば使えるかもしれませんが、当社は自作することにしました。
また、コスト面からもCertbotを動かすためのEC2インスタンスは起動したくなく、ステートレスなLambda関数上で完結するようにしました。
解決策:Lambda実装「acme-to-acm」
以下がプログラムです。 https://github.com/onocomm/acme-to-acm
プログラムはAWS CDKでデプロイが可能です。Lambda関数にはDockerイメージをアップロードする形を取っており、そのため、デプロイする手元の環境にはDocker Desktopなどの環境が必要となります。
Docker Desktop: https://www.docker.com/ja-jp/products/docker-desktop/
実際にデプロイして証明書を取得してみる
※AWS CLI及びAWS CDKはインストール済みの前提です。
1. リポジトリのクローン
git clone git@github.com:onocomm/acme-to-acm.git
2. 必要モジュールのインストール
npm install
3. 初回のみCDK Bootstrap
cdk bootstrap aws://[ACCOUNT-ID]/us-east-1
上記ではバージニア州リージョンにデプロイする予定でus-east-1に対して実行しています。本プログラムでは指定がない場合は、CloudFrontディストリビューションのACMを対象にすることを前提にスタックはすべてus-east-1にデプロイされます。リージョン指定で別のリージョンにデプロイすることも可能です。
4. デプロイ
npm run deploy

デプロイ完了後、バージニア州リージョンのLambdaコンソールを確認するとcmeToAcmCertificateRenewerという関数が作成されているのが確認出来ます。

初期設定と発行フロー
Lambda関数上でテスト機能を使用して実行してみます。実際はCloudShell上からAWS CLIなどを使って実行してもよいかと思います。
5. ACMEアカウント登録(初回のみ)
以下のようなPayloadを指定します。
{
"input": {
"mode": "register",
"email": "xxxx@xxxx.co.jp",
"server": "https://acme.amecert.jprs.jp/DV/getDirectory",
"eabKid": "xxxx",
"eabHmacKey": "xxxx"
}
}

IDやKeyはJPRS指定事業者よりACME利用者に向けて提供される利用者IDの認証情報です。この認証情報を使ってCertbot上でJPRSを認証プロバイダとして登録します。
この実行は1回のみで、デプロイしたプログラムで1つの認証情報のみを使って登録出来ます。異なる利用者IDを複数使いたい場合は、別のスタックとして複数プログラムをデプロイする必要があります。プレフィクスを切って複数デプロイする方法がありますので、詳しくはGitHubリポジトリにあるREADME.mdをご確認ください。
6. 証明書の初回発行
登録が済めば、初回の証明書発行申請を行います。続いて以下のようなPayloadで実行を行います。
{
"input": {
"mode": "certonly",
"domains": ["xxxx.com", "www.xxxx.com"],
"email": "xxxx@xxxx.co.jp",
"server": "https://acme.amecert.jprs.jp/DV/getDirectory",
"route53HostedZoneId": "xxxx"
}
}

modeをregisterからcertonlyにすることにより、登録モードから証明書発行モードに切り替えを行います。必要となるのは証明書を発行するドメイン名、プロバイダであるサーバー情報、DNS認証を行う際のRoute53のIDです。ACMのARNも指定出来ますが、指定をしない場合は新規に作成されるので省略が可能です。
実行が完了したら、ACMのコンソールを確認してください。すると証明書が新しく登録されています。現在証明書の有効期限は90日となっており、残期間が30日より少なくなるとCertbotによる入れ替えが行われます。ゆくゆくはこの期限が47日より短くなるものと思われます。

運用:自動更新・通知・監視
- 自動実行:AWS CDKで一緒にデプロイされたイベントスケジューラによって、1週間に1回CertbotがLambda上で実行され、証明書の更新が必要な場合は認証と再インポートを自動的に行ってくれます。
- 通知:このプログラムにはSNSによる通知機能がついており、発行や更新が失敗した場合に通知が送られますので、Eメールなどのサブスクリプションを登録しておくとエラーを確認出来ます。
- ログ:上手く動かないときなどはCloudWatchLogsのロググープ「
/aws/lambda/AcmeToAcmCertificateRenewer」に記録があるのでご確認ください。

今後の展望
当社では、引き続き、CloudWatch Syntheticsを使って、証明書の監視などを行う構成を組みたいと考えて居ます。また、こちらの構成についてはブログで発表させていただきたいと思います。