ほとんどの場合、AWS Certificate Manager(ACM)以外に必要なものはないが、顧客のドメインをALBでホストしていて、顧客からSSL証明書を取得する手段がない場合など、いくつかのケースでは回避策が必要になる。この記事では、AWS Application Load BalancerでLet's Encrypt SSLを発行し、それを自動化する方法を解説する。

Let's EncryptにSSL証明書をリクエストする際、LEにはweb-challenge、route53などの検証方法がある。このシナリオでは、ドメインのDNS管理が自分の手にないため、web-challengeを使用することにした。私はアプリケーションをホストしているだけで、SSLを発行する必要がある。

web-challengeでSSLをリクエストする場合、Let's Encryptは https://domain.com/.well-known/acme-challenge パスをチェックして確認・検証を行う。これはどういう意味か?つまり、Let's Encryptがポート80でドメインにリクエストを送信し、検証を行うということだ。うーん、Load Balancerの設定にいくつか調整が必要で、acme-challengeリクエストを特別にリダイレクトするための別のTarget Groupが必要になる。

Step 1: 新しいEC2を起動し、新しいTarget Groupを作成する

リポジトリをクローンする

簡単な管理と全ドメインで同じ方法で標準を維持するために、bashスクリプトを書いた。GitHubリポジトリをチェックするか、/opt/パス配下のacme-challange-serverにリポジトリをクローンしてほしい。

Tip: # git clone https://github.com/flightlesstux/alble.git

nginx、Let's Encryptをインストールして設定する

/.well-known/acme-challenge/* リクエストを受け取ると、nginxがインストールされたEC2がすべてのacme-challengeリクエストを処理し、SSLを発行する。

nginxをインストールし、/.well-known/acme-challenge/ 用に設定する

amazon-linux-extras install nginx1 コマンドでnginxを簡単にインストールできる。インストール後、nginxのルート設定ファイルに新しいlocationブロックを追加する。ルート設定ファイルは /etc/nginx/nginx.conf にある。

        location /.well-known/acme-challenge {
           root /opt/alble/certbot-challange;
        }

server_name の値は全ドメインに対して _; にする必要がある。静的なDNS名は設定できない。

nginxモジュール付きでCertbotをインストールする

まず、epelリポジトリからcertbotをインストールする。最初にepelをインストールする必要がある。epelのインストールには amazon-linux-extras install -y epel コマンドを使い、その後 yum install -y nginx certbot python2-certbot-nginx jq dig コマンドを実行して前提条件をカバーする。

acme-challengeサーバー用のIAMロールポリシー

このacme-challengeサーバーには、Let's Encrypt SSLでACMとALBの操作にアクセスするためのIAMロールがある。IAMポリシーは以下の通り。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSLEPolicy1",
            "Effect": "Allow",
            "Action": [
                "acm:DescribeCertificate",
                "acm:RemoveTagsFromCertificate",
                "acm:GetCertificate",
                "acm:AddTagsToCertificate",
                "acm:ListCertificates",
                "acm:ImportCertificate",
                "acm:ListTagsForCertificate"
            ],
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy2",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:RemoveTags",
                "elasticloadbalancing:DescribeTags",
                "elasticloadbalancing:AddTags",
                "elasticloadbalancing:AddListenerCertificates"
            ],
            "Resource": "AWS::ALB::ARN",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy3",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeSSLPolicies",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeListenerCertificates"
            ],
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy4",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:RemoveTags",
                "elasticloadbalancing:DescribeTags",
                "elasticloadbalancing:AddTags"
            ],
            "Resource": "AWS::ALB::ARN",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy5",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeSSLPolicies",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:AddListenerCertificates",
                "elasticloadbalancing:DescribeListenerCertificates"
            ],
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        }
    ]
}

Elastic IPを割り当てた後、このポリシーをセキュリティ上の問題や漏洩なく直接acme-challengeサーバーにアタッチできる。ローカル認証情報を使用したい場合も、同じIAMポリシーを使用できる。

新しいTarget Groupを作成する

EC2インスタンスの設定後、acme-challengeという名前のTarget Groupを作成し、acme-challange-serverという名前のnginxがインストールされたEC2にリクエストをリダイレクトし、acme-challenge-serverをターゲットとして登録する。すべてが問題なく進めば、画面は以下のようになる。

Step 2: Application Load Balancerのリスナールールを作成する

/.well-known/acme-challenge/* リクエストを受け入れてacme-challenge Target Groupにリダイレクトするために、新しいリスナールールを編集・作成する必要がある。このステップが完了すると、画面は以下のようになる。

これで、ACMEからLet's Encrypt SSLをリクエストし、問題なく発行してACMにインポートし、AWS ALBに割り当てる準備が整った。この操作には発行のための3つの異なるステップが含まれる。

  1. Let's EncryptからSSLをリクエストする
  2. ドメインまたはサブドメインがAWS ALB CNAMEレコードと一致する場合に、リクエストしたSSLをインポートする
  3. 本番で使用するためにインポートしたSSL証明書をAWS ALBに割り当てる

ACMEからLet's Encrypt SSLを発行・リクエストする

gitリポジトリをクローンしたら、/opt/alble/renewal-hooks フォルダを/etc/letsencrypt/パス配下に移動する必要がある。そうしないと、ACMで証明書を更新できず、自分と顧客のセキュリティが損なわれる。mv /opt/alble/renewal-hooks /etc/letsencrypt/ をコピー&ペーストすればよい。

gitリポジトリのREADME.mdファイルを読み、envファイルに変数を記述するだけだ。その後、AWS Application Load BalancerでLet's Encrypt SSL証明書を簡単に実行・管理・作成できる。SSLを発行するためにacme-challange-serverにコマンドを実行するにはAWS SSMの使用を推奨する。そのためには、IAMロールにAmazonEC2RoleforSSM ポリシーを追加する必要がある。

テストにはサブドメインを使用することにしたが、問題ではない。ルートドメインも使用できる。テストドメインは awsle-1.ercanermis.com と awsle-2.ercanermis.com だ。

/opt/alble/ パスにいる状態で、./create-new-site.sh awsle-1.ercanermis.com のようにコマンドを実行すると、以下のような出力が表示される。

ALBLeスクリプトは、続行する前にまずドメインおよび/またはサブドメインのCNAMEレコードをチェックして確認する。間違ったドメイン/サブドメインを入力したりタイプミスした場合は、Slackアラートで警告する。アラートは自動化にとって本当に重要だ。envファイルでSlackアラートを設定できる。以下は以前にLet's Encrypt SSLを発行した場合の例:

本番環境

AWS ALBのDNS名は web-application-elb-1302305711.us-east-1.elb.amazonaws.com で、https://web-app.ercanermis.com からアクセスできる。SSLの取得にはAWS Certificate Managerを使用している。

テストドメインは https://awsle-1.ercanermis.com と https://awsle-2.ercanermis.com で、AWS Application Load BalancerでLet's Encryptを使用している。やった!

https://web-app.ercanermis.com はAmazon提供のSSLを使用している
https://awsle-1.ercanermis.com はLet's Encrypt提供のSSLを使用している
https://awsle-2.ercanermis.com はLet's Encrypt提供のSSLを使用している

AWS ALB SSL証明書とACMはどのように見えるか?

ALB SSL証明書のスクリーンショット
Amazon Certificate Managerのスクリーンショット

この記事が役立つことを願っている。P.S. 更新リクエスト用のcronを設定するのを忘れずに。
ソースコード: https://github.com/flightlesstux/alble/