AWS EC2でアクセス元IP制限つきWebサーバーのLet’s Encrypt証明書の取得、自動更新設定(HTTP認証)

isdはじめに

HTTP(S)のアクセス元IPアドレスを限定して運用している、プライベート向けWebシステムが、AWS EC2上で稼働しているものとします。
このWebサーバー上で、Let’s Encrypt証明書を取得、運用する方法についてまとめます。

クラウドサービスはAWS、サーバーOSはAmazon Linuxを前提としますが、ファイアウォールを操作するAPIが公開されていれば、AWS以外のサービスでも同じ方針で解決できるでしょうし、OSもLinux系であればほぼ同じ設定でいけるでしょう。

各コマンドはrootユーザーで実行することを想定しています。

なお、

  • Let’s Encryptとは何か?
  • Let’s Encryptクライアント(Certbot)のインストール
  • Apache等WebサーバーソフトウェアのSSL使用設定

については、既にネット上に多くの情報があるので、ここでは省略します。
以下の「さくらのナレッジ」の記事は、わかりやすいくまとまっていると思います。

・Let’s EncryptのSSL証明書で、安全なウェブサイトを公開
https://knowledge.sakura.ad.jp/5573/

isdドメイン認証方式「HTTP認証」と「DNS認証」

Let’s Encrypt証明書を取得する際のドメイン認証方式としては、「HTTP認証」と「DNS認証」があります。
「HTTP認証」の場合、Let’s Encryptのサーバー側からWebサーバーに対して http://<Common Name>/~ にアクセスすることで、ドメイン所有者が発行申請を行ったことを確認します。
ところが、今回のケースでは、HTTPのアクセス元IPアドレスを限定していますから、そのままでは「HTTP認証」がエラーとなり、証明書を取得できません。

このようなケースでは、「DNS認証」を使うのがひとつの解決方法となります。
Let’s Encryptが指定したワンタイムトークンをDNSのTXTレコードとして登録し、Let’s Encryptのサーバー側でそのレコードが確認できれば、証明書が取得できます。

※ワイルドカード証明書については、「HTTP認証」では取得できず、「DNS認証」のみとなります。

DNSサーバーとしてAWS Route 53を使用しているのであれば、certbot-dns-route53というCertbotのプラグインを使用して、証明書を取得、更新できます。
「AWSのAPIを利用して、認証に必要なTXTレコードをRoute 53のホストゾーンに追加し、認証が完了したら削除する」という処理を、このプラグインが自動的にやってくれるというわけですね。

certbot-dns-route53プラグインを利用した「DNS認証」の設定手順については、以下の記事がわかりやすいです。

・certbotを使って Let’s Encrypt ワイルドカード証明書を発行する(Route 53)
https://www.kemonox.net/archives/51

「DNS認証」が使えるのであれば、それで解決します。

ここでは、

  • (何らかの理由で)DNSレコードの自動編集ができない。
  • ツールにDNSレコードの編集権限を与えるのは、セキュリティ上好ましくない。

といった場合に、「HTTP認証」でLet’s Encrypt証明書を取得する方法を説明します。

先ほど、
「HTTPのアクセス元IPアドレスを限定しているから、そのままでは「HTTP認証」がエラーとなる」
と書きましたが、これをどうやって解決するかというと、証明書の取得・更新時に、一時的にHTTPへのアクセス元IPアドレスの限定をやめて、全てのIPアドレスに公開します。
シンプルなやり方ですね。

※Let’s Encryptからのアクセス元IPアドレスは非公開のため、全てのIPアドレスに公開する必要があります。

HTTPポートを全公開することによるセキュリティリスクについては、のちほど説明します。

isd運用方針

通常時と証明書取得・更新時の状態、動作は次のようになります。

  • 通常時
    • Apache等のWebサーバーソフトウェアを起動
    • セキュリティグループで、HTTP(TCP/80),HTTPS(TCP/443)のアクセス元IPアドレスを限定
  • Let’s Encrypt証明書の取得・更新時(10~30秒間ぐらい)
    • Apache等のWebサーバーソフトウェアをを停止(Certbotのstandaloneプラグインと使用ポートTCP/80が被るため)
    • 全IPアドレスからHTTP(TCP/80)を許可するセキュリティグループをWebサーバーのEC2インスタンスに一時的にアタッチ、処理終了時にデタッチ
    • Certbotのstandaloneプラグイン、HTTP認証で証明書を取得、更新(このときstandaloneプラグインがTCP/80ポートを使用)
    • これら一連の操作を行うシェルスクリプトを作成し、cronで定期実行

 
※WebサーバーソフトApacheは、Nginxでも何でも構いません。適宜置き換えてください。

Let’s Encrypt証明書の取得、更新時は、Apache(もしくはその他のWebサーバーソフトウェア)を停止するため、一時的にWebシステムへのアクセスができなくなります。
ですので、証明書の自動更新は、深夜・早朝など、Webシステムの利用が少ない時間帯に実行するとよいでしょう。

また、10~30秒間程度の短時間とはいえ、一時的にサーバーへのHTTPアクセスを全IPアドレスに公開することで、不正アクセス等、セキュリティ上の不安が生じるかもしれません。
でも、上記のとおり、Apache(もしくはその他のWebサーバーソフトウェア)を停止して、その代わりにCertbotのstandaloneプラグインを起動しています。
Certbotのstandaloneプラグインは、このサーバーで本来稼働しているWebシステムのDocumentRootを知りませんから、Webシステムにはアクセスしようがありません。

ですので、TCP/80ポートやそのときに起動しているCertbotのstandaloneプラグインへの何らかのアクセスはあるかもしれませんが、Webシステム自体への不正アクセスは生じません。

なお、Certbotのstandaloneプラグインではなく、webrootプラグインを使えば、Apacheを起動したままHTTP認証ができますが、そうすると、一時的に第三者からのWebシステムへのアクセスが可能となってしまいますので、好ましくありません。

isd事前準備

DNSの設定

証明書のCommonNameで、このWebサーバーにアクセスできるよう、DNSレコードを設定します。
Webシステムを構築済みであれば、設定済みのはずですね。

Let’s Encryptクライアント(Certbot)のインストール

EPELリポジトリや、GitHub、EFF等、お好みのところからCertbotをダウンロード、インストールします。
ここでは記載しませんので、他の記事等を参照してください。

インストール後は、helpサブコマンドなどで、最低限のコマンド実行を確認しておくとよいでしょう。
インストール方法によっては、Python環境の問題で実行エラーとなることもありますので。

 # ./certbot-auto --help

 

HTTPを全IPアドレスに公開するセキュリティグループを作成

AWSマネジメントコンソールで、
「ソース: 0.0.0.0/0 から HTTP(TCP/80) へのインバウンドアクセスを許可する」
セキュリティグループを作成します。

EC2インスタンスへのアタッチは、ここではしなくてもよいです。

セキュリティグループの変更権限設定

Let’s Encrypt証明書の取得・更新時、Webサーバー自身のEC2インスタンスにセキュリティグループをアタッチ、デタッチする操作を行います。
自動更新の際はAWS CLIを使用するので、AWSマネジメントコンソールで、必要な権限を含むIAMポリシーを作成し、EC2ロールもしくはIAMユーザー、グループにアタッチします。

EC2インスタンスのセキュリティグループは、EC2のModifyInstanceAttribute APIアクションで変更できますので、この権限をもつIAMポリシーを作成します。

JSON形式ではこんな感じになります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "ec2:ModifyInstanceAttribute",
            "Resource": "*"
        }
    ]
}

 

このポリシーで操作できるEC2インスタンスを限定するのであれば、”Resource”: のところで、以下のようにインスタンスIDを指定するとよいでしょう。

            "Resource": "arn:aws:ec2:<Region>:<Account-ID>:instance/<Instance-ID>"

 

このIAMポリシーを、EC2インスタンスに適用済みのEC2ロールにアタッチします。
EC2ロールを使用しないのであれば、IAMポリシーをIAMユーザー(もしくはIAMグループ)にアタッチして、IAMユーザーのアクセスキーを取得し、Webサーバー上で、aws configure コマンドを実行してアクセスキーを設定します。

AWS CLIのインストール、(EC2ロールを使用しない場合の)アクセスキーの設定については、AWS CLIのユーザーガイド等を参照してください。

・AWS Command Line Interface のインストール
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/installing.html

・AWS CLI の設定
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-getting-started.html

isd証明書の取得

以下を順に実行します。

  1. Apacheの停止
  2. HTTPを全IPアドレスに公開するセキュリティグループをアタッチ
  3. Let’s Encrypt証明書を取得
  4. HTTPを全IPアドレスに公開するセキュリティグループをデタッチ
  5. Apacheを起動

また、

  • Certbotコマンドのパス: /usr/local/certbot/certbot-auto
  • AWSリージョン: ap-northeast-1
  • WebサーバーのEC2インスタンスID: i-XXXXXXXXXXXXXXXX
  • EC2インスタンスに適用済みのセキュリティグループID: sg-AAAAAAAA
  • HTTPを全IPアドレスに公開するセキュリティグループID: sg-ZZZZZZZZ
  • 証明書のCommonName: www.example.com

とします。
適宜、置き換えてください。

1. Apacheの停止

 # /etc/init.d/httpd stop

 

2. HTTPを全IPアドレスに公開するセキュリティグループをアタッチ

AWSマネジメントコンソールで操作してもよいのですが、自動更新時はAWS CLIを使用するので、その動作確認を兼ねて、AWS CLIでEC2インスタンスにセキュリティグループをアタッチします。
EC2インスタンスにもともとアタッチ済みのセキュリティグループID(sg-AAAAAAAA)も併せて指定しないと外れてしまうので注意してください。
(SSHアクセスもできなくなってしまいます)

もともと複数のセキュリティグループをアタッチしていた場合は、スペース区切りですべて指定します。

 # aws ec2 modify-instance-attribute \
    --region ap-northeast-1 \
    --instance-id i-XXXXXXXXXXXXXXXX \
    --groups sg-AAAAAAAA sg-ZZZZZZZZ

 

実行後は、AWSマネジメントコンソールで、アタッチされているセキュリティグループを確認するとよいでしょう。
実行エラーとなる場合は、IAMポリシーの権限まわりか、AWS CLIの設定の問題が考えられますので、エラーメッセージを参考に、調査、対応してください。

3. Let’s Encrypt証明書を取得

Certbotコマンドのサブコマンドcertonlyで、証明書を取得します。
指定するオプションは以下のとおりです。

  • –authenticator standalone: 認証時のプラグインは standalone
  • –preferred-challenges http: 認証方式はHTTP
  • –agree-tos: 利用規約の確認画面を表示せずに同意する
  • –register-unsafely-without-email: Let’s Encryptにメールアドレスを登録しない(登録する場合は、-m オプションでメールアドレスを指定します)
 # /usr/local/certbot/certbot-auto certonly \
    --authenticator standalone \
    --preferred-challenges http \
    --agree-tos --register-unsafely-without-email \
    -d www.example.com

 

証明書ファイルが存在することを確認します。

 # ls -l /etc/letsencrypt/live/www.example.com/

lrwxrwxrwx. 1 root root  45  4月 22 13:27 cert.pem -> ../../archive/www.example.com/cert1.pem
lrwxrwxrwx. 1 root root  46  4月 22 13:27 chain.pem -> ../../archive/www.example.com/chain1.pem
lrwxrwxrwx. 1 root root  50  4月 22 13:27 fullchain.pem -> ../../archive/www.example.com/fullchain1.pem
lrwxrwxrwx. 1 root root  48  4月 22 13:27 privkey.pem -> ../../archive/www.example.com/privkey1.pem

 

4. HTTPを全IPアドレスに公開するセキュリティグループをデタッチ

AWS CLIでEC2インスタンスのセキュリティグループを元に戻します。

 # aws ec2 modify-instance-attribute \
    --region ap-northeast-1 \
    --instance-id i-XXXXXXXXXXXXXXXX \
    --groups sg-AAAAAAAA

 

5. Apacheを起動

 # /etc/init.d/httpd start

 

証明書が正しく取得できたら、ApacheのSSL用Config(ssl.confなど)で、秘密鍵、証明書、中間証明書ファイルのパス(/etc/letsencrypt/live/www.example.com/*.pem)を指定し、WebブラウザからHTTPSアクセスができることと、ブラウザで証明書の情報を確認します。

isd証明書の自動更新

証明書を更新するスクリプトを作成して、cronで定期実行するようにします。

証明書更新スクリプトの作成

証明書を取得したときと同様のコマンドを一つずつ実行するスクリプトを作成します。

Certbotコマンドによる証明書の更新は、サブコマンドrenewを使用します。
証明書取得時のオプションは、最初に取得したときにConfig(/etc/letsencrypt/renewal/www.example.jp.conf)に保存されているので、更新時の細かい指定は不要です。
ただし、自動実行させるので、対話入力が不要となるよう –non-interactive をつけています。

※(2018.7.23追記)cronの実行環境によってはAWS CLIが正しく実行されない可能性があったので、AWS CLIをフルパス(/usr/bin/aws)指定で実行するよう修正しました。

 # vi /root/bin/update_sslcert.sh

--
#!/bin/bash
#
LOGFILE=/var/log/update_sslcert.log
CERTBOT_CMD=/usr/local/certbot/certbot-auto
AWS_CMD=/usr/bin/aws
WEBSTOP_CMD="/etc/init.d/httpd stop"
WEBSTART_CMD="/etc/init.d/httpd start"

INSTANCE_ID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`
REGION='ap-northeast-1'
ORG_SG='sg-AAAAAAAA'
HTTPALL_SG='sg-ZZZZZZZZ'

MAILTO=root

echo "===== Update SSL Cert =====" >> ${LOGFILE}
echo "`date` Update SSL Cert start" >> ${LOGFILE}

# 1. Apacheの停止
${WEBSTOP_CMD} >> ${LOGFILE}

${AWS_CMD} ec2 modify-instance-attribute \
--region ${REGION} \
--instance-id ${INSTANCE_ID} \
--groups ${ORG_SG} ${HTTPALL_SG} >> ${LOGFILE}

sleep 3

# 3. Let's Encrypt証明書の更新
${CERTBOT_CMD} renew \
  --non-interactive >> ${LOGFILE}
LE_STATUS=$?

if [ "$LE_STATUS" != 0 ]; then
    echo "Update SSL Cert failed" |\
    mail -s "Update SSL Cert in `hostname`" ${MAILTO}
fi

# 4. HTTPを全IPアドレスに公開するセキュリティグループをデタッチ
${AWS_CMD} ec2 modify-instance-attribute \
--region ${REGION} \
--instance-id ${INSTANCE_ID} \
--groups ${ORG_SG} >> ${LOGFILE}

# 5. Apacheの起動
${WEBSTART_CMD} >> ${LOGFILE}

echo "`date` Update SSL Cert end" >> ${LOGFILE}

# EOF
--

 

スクリプトについて補足します。
ログを /var/log/update_sslcert.log に書き出します。
EC2のインスタンスIDは、インスタンスメタデータから取得していますが、インスタンスIDを直接書いてもよいでしょう。
セキュリティグループをアタッチしたあとは、即時反映されるはずですが、念のため3秒のスリープを入れています。
何らかの原因で証明書の更新に失敗したときは、MAILTOで指定した宛先に、「Update SSL Cert in <hostname>」というSubjectのアラートメールを送信します。
(WebサーバーでPostfix等によるメール送信設定が必要です。)

念のため、シェルスクリプトの文法チェックを行い、問題なければ実行権限を付与します。

 # /bin/bash -n /root/bin/update_sslcert.sh

 # chmod 755 /root/bin/update_sslcert.sh

 

証明書更新スクリプトの動作確認

スクリプトを単体で実行してみます。

実行する前に、Certbotのrenewサブコマンドでは、証明書は有効期限が残り30日未満にならないと更新されないため、強制的に更新するよう、–force-renewalオプションを追加します。

 # vi /root/bin/update_sslcert.sh

-- 変更前
...

${CERTBOT_CMD} renew \
  --non-interactive >> ${LOGFILE}
--

-- 変更後
...

${CERTBOT_CMD} renew \
  --non-interactive --force-renewal >> ${LOGFILE}
--

 

証明書更新スクリプトを実行します。

 # /root/bin/update_sslcert.sh

 

psコマンドで、Apacheプロセスのタイムスタンプを見て再起動されたことを確認します。

 # ps aux | grep httpd

root     12091  0.1  0.3 168848  6412 ?        Ss   16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12092  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12093  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12094  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12095  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12096  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12097  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12098  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
apache   12099  0.0  0.1 170932  3192 ?        S    16:13   0:00 /usr/sbin/http  -DFOREGROUND
--

 

Apacheのエラーログを見ると、Apacheが停止していた時間(この場合は11秒間)がわかります。

 # less /var/log/httpd/error_log

[Thu Apr 22 16:13:32.277651 2018] [mpm_prefork:notice] [pid 11950] AH00170: caught SIGWINCH, shutting down gracefully
[Thu Apr 22 16:13:43.285859 2018] [auth_digest:notice] [pid 12091] AH01757: generating secret for digest authentication ...
[Thu Apr 22 16:13:43.290102 2018] [mpm_prefork:notice] [pid 12091] AH00163: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips configured -- resuming normal operations
[Thu Apr 22 16:13:43.290124 2018] [core:notice] [pid 12091] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND'

 

証明書ファイルを確認します。
証明書ファイル群へのシンボリックリンク先ファイル名の世代数が、1から2に変わっており、シンボリックリンクのタイムスタンプも証明書更新スクリプトの実行時となっていることがわかります。

 # ls -l /etc/letsencrypt/live/www.example.com/

-rw-r--r--. 1 root root 543  4月 22 13:27 README
lrwxrwxrwx. 1 root root  45  4月 22 16:13 cert.pem -> ../../archive/www.example.com/cert2.pem
lrwxrwxrwx. 1 root root  46  4月 22 16:13 chain.pem -> ../../archive/www.example.com/chain2.pem
lrwxrwxrwx. 1 root root  50  4月 22 16:13 fullchain.pem -> ../../archive/www.example.com/fullchain2.pem
lrwxrwxrwx. 1 root root  48  4月 22 16:13 privkey.pem -> ../../archive/www.example.com/privkey2.pem

 

証明書更新スクリプトのログを確認します。
「Congratulations, all renewals succeeded.~」と出力されているので、証明書が正しく更新されたことがわかります。

 # less /var/log/update_sslcert.log

--
===== Update SSL Cert =====
2018年  4月 22日 日曜日 16:13:32 JST Update SSL Cert start

-------------------------------------------------------------------------------
Processing /etc/letsencrypt/renewal/www.example.com.conf
-------------------------------------------------------------------------------

-------------------------------------------------------------------------------
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/www.example.com/fullchain.pem
-------------------------------------------------------------------------------

-------------------------------------------------------------------------------

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/www.example.com/fullchain.pem (success)
-------------------------------------------------------------------------------
2018年  4月 22日 日曜日 16:13:43 JST Update SSL Cert end

 

念のため、WebブラウザからHTTPSアクセスができることと、ブラウザで証明書の情報を確認します。

ひととおり確認して問題なければ、自動更新スクリプトで、強制的に更新するよう追加した –force-renewalオプションを削除します。

 # vi /root/bin/update_sslcert.sh

-- 変更前
...

${CERTBOT_CMD} renew \
  --non-interactive --force-renewal >> ${LOGFILE}
--

-- 変更後
...

${CERTBOT_CMD} renew \
  --non-interactive >> ${LOGFILE}
--

 

証明書自動更新の定期実行設定

自動更新スクリプトを定期実行するよう、rootユーザーのcronエントリに設定します。
以下は、毎週木曜日の6:00に実行する設定例です。

 # crontab -e

--
# Update SSL Cert
0 6 * * 4 /root/bin/update_sslcert.sh 1> /dev/null 2>&1
--

 

証明書の有効期限が30日未満であれば、証明書が更新されます。
30日以上のときは更新せず、Apacheの停止→起動とセキュリティグループのアタッチ、デタッチ操作のみ行います。

以上で、ひととおりの設定と動作確認は完了です。

isdおわりに

AWS EC2上で稼働し、HTTP(S)のアクセス元IPアドレスを限定して運用している、プライベート向けWebシステムで、Let’s Encrypt証明書をDNS認証ではなくHTTP認証で取得、自動更新する設定についてまとめました。

certbot-dns-route53プラグインによるDNS認証は、設定がそれほど難しくなく、自動更新には便利な方法です。
ですが、個人的には「DNSレコードの編集」というセキュリティ上非常に重要な機能権限を、いちツールに与えてよいのだろうか、というちょっとした不安もあります。
また、何らかの理由で、DNSサーバーとしてRoute 53を使用していないケースもあるでしょう。

そういう場合に、今回紹介した方法がひとつの解決方法となるかと思います。

ほかの解決方法としては、例えば
「ACMで取得した証明書をCloudFrontに設定して、WebアクセスをすべてCloudFront経由、EC2オリジンの構成とする」
といった方法が考えられます。
 

(関連記事)
・Let’s EncryptによるSSLサーバー証明書の自動更新設定
https://inaba-serverdesign.jp/blog/20160630/lets-encrypt_update_sslcert.html
 

Follow me!