Python + boto3でメール検証してるACMの証明書を列挙する

PythonとBoto3を使って、Eメール検証しているACMの証明書を列挙してみました。対象のアカウント・リージョンが多い場合にSwitchRoleやリージョンの切り替えをせずに確認できます。
2018.09.30

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

はじめに

私が参加しているプロジェクトはDNS検証に対応する以前からACMの証明書を利用していて、一部Eメール検証のものが残っています。証明書の更新を自動化するため、証明書の期限切れ通知を受け取ったタイミングで逐次DNS検証に切り替えていたのですが、都度対応するのが面倒になってきました。

そこで、切り替えが必要な証明書がどれくらい残っているか確認するため、AWS SDK for Python (Boto3)を使って、Eメール検証の証明書を列挙してみます。

前提条件

  • macOS: 10.13.6
  • CPython: 3.7.0
  • Pipenv: 2018.7.1
  • boto3: 1.9.14

環境構築

Pipenv を使って環境構築します。

$ pipenv install --python 3.7.0 boto3 colorama

colorama は表示を見やすくするために使います。

やってみる

AWSアカウントごと・リージョンごとにEメール検証の証明書を列挙してみます。 ACMのドキュメント を確認すると、 ListCertificatesDescribeCertificate で 実現できそうです。

以下の手順で処理するプログラムを作成します。

  1. 任意のプロファイル・任意のリージョン用のACMクライアントを作成
  2. ACMの証明書一覧を取得する
  3. 各証明書の詳細情報を取得する
  4. ドメイン検証オプションの ValidationMethodEMAIL の場合、切り替えが必要と伝わるように表示する
def main():
    profiles = ["default", "staging"]
    regions = ["ap-northeast-1", "us-east-1"]
    results = [r for r in enumerate_certificates(profiles, regions)]
    show_results(results)

enumerate_certificates で手順1〜3の処理を、 show_results で手順4の処理を行います。以降で enumerate_certificatesshow_results を実装していきます。

完成版のソースは Gist に載せています。

各プロファイル・各リージョンの証明書を列挙する

各プロファイル・各リージョンのACMクライアントを作って、一覧取得・詳細取得のAPIを呼び出します。 Boto3のレスポンスはdict型なのでそのまま使っても良いのですが、(自分にとって)わかりやすいnamedtupleを使うようにしています。 enumerate_certificates の戻り値も同様です。

import itertools
from collections import namedtuple
from boto3.session import Session

EnumerateResult = namedtuple("EnumerateResult", ["profile", "region", "certificates"])

def enumerate_certificates(profiles, regions):
    for p, r in itertools.product(profiles, regions):
        # ACMのクライアントを生成
        session = Session(profile_name=p, region_name=r)
        acm = session.client("acm")

        # 証明書の一覧を取得
        summaries = list(list_certificates(acm))

        # 証明書の詳細を取得
        certificates = [describe_certificate(acm, s.arn) for s in summaries]

        # 結果を返却する
        yield EnumerateResult(profile=p, region=r, certificates=certificates)

証明書の一覧を取得する (ListCertificates)

任意のプロファイル・リージョン用のACMクライアントを使って、証明書の一覧を取得します。

from collections import namedtuple

CertificateSummary = namedtuple("CertificateSummary", ["arn", "domain_name"])

def list_certificates(acm):
    def create_summary(s):
        arn = s["CertificateArn"]
        domain_name = s["DomainName"]
        return CertificateSummary(arn=arn, domain_name=domain_name)

    complete = False
    next_token = None
    while not complete:
        params = dict(NextToken=next_token) if next_token else {}
        res = acm.list_certificates(**params)
        next_token = res.get("NextToken", None)
        complete = next_token is None

        for x in res["CertificateSummaryList"]:
            yield create_summary(x)

証明書の詳細情報を取得する (DescribeCertificate)

詳細情報の取得も同様です。ACMのクライアントに加えて、対象の証明書のARNを使います。

from collections import namedtuple

Certificate = namedtuple(
    "Certificate", ["arn", "domain_name", "status", "validation_options"]
)
ValidationOption = namedtuple(
    "DomainValidationOptions", ["validation_status", "validation_method"]
)

def describe_certificate(acm, cert_arn):
    def create_validation_option(o):
        status = o["ValidationStatus"]
        method = o["ValidationMethod"]
        return ValidationOption(validation_status=status, validation_method=method)

    res = acm.describe_certificate(CertificateArn=cert_arn)
    cert = res["Certificate"]

    options = cert.get("DomainValidationOptions", [])
    options = [create_validation_option(x) for x in options]

    return Certificate(
        arn=cert_arn,
        domain_name=cert["DomainName"],
        validation_options=options,
        status=cert["Status"],
    )

結果を表示する

最後に結果を表示します。証明書のステータスが ISSUED のものを対象に、 ドメインの検証方法が DNS の場合は を、 MAIL の場合は を、それぞれ先頭に表示します。

import itertools
import colorama
from colorama import Fore

def prefix_for(cert):
    def is_issued():
        return cert.status == "ISSUED"

    def has_mail_validation():
        return any([o.validation_method == "EMAIL" for o in cert.validation_options])

    if is_issued():
        prefix = Fore.RED + "✗" if has_mail_validation() else Fore.GREEN + "✓"
        prefix += Fore.RESET
        return prefix
    else:
        return "-"


def show_results(results):
    colorama.init(autoreset=True)

    status_width = max([
        len(c.status)
        for c in itertools.chain.from_iterable([x.certificates for x in results])
    ])
    for profile, profile_group in itertools.groupby(results, key=lambda x: x.profile):
        if not profile_group:
            continue

        print("-" * 40)
        print(f"[{profile}]")
        for result in profile_group:
            if not result.certificates:
                continue

            print(f"\n{result.region}")
            for c in result.certificates:
                prefix = prefix_for(c)
                status = c.status.ljust(status_width)
                methods = ",".join([o.validation_method for o in c.validation_options])
                print(f"{prefix} {status} {c.domain_name} ({methods})")

動かすとこんな感じに表示します。

赤字で 付きの証明書が対象のものです。

おわりに

今回はAWS SDK for Python (Boto3) を使って面倒な作業をスクリプト化してみました。画面から操作すると面倒なことでも簡単にプログラム化できることが多いので積極的にツール化していこうと思います。