AWS
AmazonLinux
aws-cli
letsencrypt

Let's Encrypt 証明書の自動発行とELB自動登録を行ったログ

More than 1 year has passed since last update.

TL;DR

  1. Let's Encrypt で証明書発行して
  2. IAM にアップロードして
  3. ELB に設定して
  4. 古い証明書を削除する

という手順を実施する Bash スクリプト (root 実行用)
https://gist.github.com/hidekuro/1801e711367b47a7e519

本稿ではこれを使うまでの道のりをダラダラと説明しています。

はじめに

AWS ELB に Let's Encrypt の証明書を登録する仕組み を自動化したので、
その作業やバッチスクリプトのソースを残しておきます。

※ 実稼働させたスクリプトから秘匿情報を間引きつつ記事に起こしていますので、不備があればご指摘下さい。

要件

  • ELB に登録するサーバー証明書の発行と更新作業を自動化したい
  • 稼働中の Web サーバーは止めたくない

前提環境

  • SSH で作業可能で、 Nginx が稼動している(稼働させる) EC2 インスタンスを持っている。
  • EC2 インスタンスの OS は AmazonLinux とする。1
  • 独自ドメインを所持しており、 ELB の CNAME が引ける状態になっている。

おおまかな流れ

  1. Let's Encrypt Client を使う準備をする。
  2. Let's Encrypt が提供するコマンドで証明書を発行する。
  3. aws-cli で証明書を IAM にアップロードする。
  4. アップロードした証明書を ELB に設定する。

以上の流れを自動化します。

実践

1. Let's Encrypt Client の準備

Let's Encrypt Client を Git で任意の場所に clone して下さい。

git clone https://github.com/letsencrypt/letsencrypt

2. letsencrypt-auto の初回起動

本稿執筆時点で AmazonLinux は実験的なサポート中のため、 letsencrypt-auto を実行すると
「いったん --debug をつけて起動してね」 という旨の警告が出て止まります。

指示に従ってデバッグフラグをつけ、ヘルプコマンドでも実施しておきます。

cd $LE_HOME
sudo ./letsencrypt-auto --debug --help

NOTE

  • letsencrypt-auto は virtualenv 上で動き、実行ユーザーの ~/.local に Python 環境を作ります。
  • 本稿のバッチは root ユーザーで実行する設計なので、この初回起動も root(sudo) でやっておきます。

エラーになる場合

もし上のコマンド実行時に OpenSSL モジュールが見つからないといったエラーが出た場合、過去の Let's Encrypt 環境が残っているせいである可能性があります。

いったん実行ユーザーの ~/.local を消してから

pip install --upgrade pip
pip install --upgrade virtaulenv

…で Python 周りの環境を更新して、 letsencrypt-auto を再実行して構築しなおさせると治るようです。

Python 2.6 / 2.7 / 3.x が共存している場合は、次のように alternatives の管理下において 2.7 をデフォルトにしておくといいと思います。

sudo update-alternatives --install /usr/bin/pip pip /usr/bin/pip-2.6 26
sudo update-alternatives --install /usr/bin/pip pip /usr/bin/pip-2.7 27
sudo update-alternatives --install /usr/bin/virtualenv virtualenv /usr/bin/virtualenv-2.6 26
sudo update-alternatives --install /usr/bin/virtualenv virtualenv /usr/bin/virtualenv-2.7 27

3. aws profile の準備

バッチに使わせる IAM ロールを発行し、 AWS Profile を準備しておきます。

本稿のバッチでは次の権限を使っていますので、これを含むバッチ用の IAM ロール/ユーザーを作成してアクセスキーとシークレットキーを保管して下さい。

  • elasticloadbalancing:SetLoadBalancerListenerSSLCertificate
  • iam:ListServerCertificates
  • iam:UploadServerCertificate
  • iam:DeleteServerCertificate

本稿の例では elb_update_cert というプロファイル名で作ることにします。

また、 letsencrypt-auto で得られる証明書は /etc/letsencrypt 配下に root:root 所有で置かれるため、バッチも root で実行する前提で進めます。

[ec2-user@foo ~] $ sudo su -

[root@foo ~] # aws configure --profile elb_update_cert
AWS Access Key ID [None]: 用意したアクセスキー
AWS Secret Access Key [None]: 用意したシークレットキー
Default region name [None]: お好みのリージョン
Default output format [None]: json

※ output = json にしていますが今回のバッチでは使ってません

作成したら Profile を確認しておきます

[root@foo ~]# aws configure --profile elb_update_cert list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile          elb_update_cert           manual    --profile
access_key     ******************** shared-credentials-file
secret_key     ******************** shared-credentials-file
    region           ap-northeast-1      config-file    ~/.aws/config

4. Nginx に穴を開ける

Let's Encrypt Client は、 -w で指定したドキュメントルートの下に一時的にファイルを書き出し、
ACME サーバー側にそれを伝えて、サーバーからの HTTP アクセスをもってドメイン認証とします。

例えば独自ドメイン yourdomain.com を持っているなら

DOCROOT/.well-known/acme-challenge/random-hash-ABCD123456789

…のようなファイルが作られ、 Let's Encrypt の ACME サーバーから

http://yourdomain.com/.well-known/acme-challenge

…に対してアクセスが成功すると、証明書を発行してもらえます。
ですので、ここには認証などを挟まずに単純アクセスできるようにしておきます。

他の location との兼ね合いにもよりますが、よくある location / { ... } で Web アプリケーションに proxy_pass する設定を書いている場合などは、下のようにして穴を開けてやる必要があります。

nginx.conf
server {
    server_name yourdomain.com;
    liten 80;

    root /var/www/my-site;
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # ...

    # for Let's Encrypt
    location ^~ /.well-known/acme-challenge {
        root /var/www/letsencrypt;
        access_log /var/log/nginx/access_letsencrypt.log;
        error_log /var/log/nginx/error_letsencrypt.log;
    }
}

前方一致正規表現などの強いルールを使って、優先順位が負けないようにしておきましょう。

5. 自動化スクリプト

ここまでで、 Let's Encrypt Client および AWS CLI を使う準備が整いましたので、下の Bash スクリプトを任意のディレクトリに配置します。

https://gist.github.com/hidekuro/1801e711367b47a7e519

/root/cronjob/elb_update_cert.sh
#!/bin/bash
set -e
cd $(dirname $0)

# 現在日付のYYYYMMDD
DATE_CURRENT_YMD=$(date '+%Y%m%d')

# AWS Profile 名
AWS_PROFILE=elb_update_cert

# 独自ドメイン
DOMAINS=(
  "yourdomain.com"
  "www1.yourdomain.com"
  "www2.yourdomain.com"
)

# IAM 上のサーバー証明書名。後ろに "-" + $DATE_CURRENT_YMD が付く。
CERT_NAME="letsencrypt-cert"

# 対象 ELB のロードバランサー名
ELB_NAME="my-site-balancer01"

# aws-cli 実行コマンド
EXEC_AWS="aws --profile elb_update_cert"

# Let's Encrypt の連絡用メールアドレス
LE_EMAIL=yourmail@example.com

# Let's Encrypt Client を clone したパス
LE_HOME=/home/ec2-user/letsencrypt

# letsencrypt-auto 実行コマンド
EXEC_LE_AUTO="${LE_HOME}/letsencrypt-auto --email $LE_EMAIL --agree-tos"

# 取得した証明書ファイルのリンク群が配置されるディレクトリ
# letsencrypt-auto に与えた最初のドメイン名が採用される
LE_FILES_ROOT=/etc/letsencrypt/live/${DOMAINS[0]}

# 証明書ファイル群のシンボリックリンクのパス
CERT_PATH=$LE_FILES_ROOT/cert.pem
CHAIN_PATH=$LE_FILES_ROOT/chain.pem
FULLCHAIN_PATH=$LE_FILES_ROOT/fullchain.pem
PRIVKEY_PATH=$LE_FILES_ROOT/privkey.pem

# DOMAINS を "-d" とペアで繋げたパラメータ (-d "yourdomain.com" -d "www1.yourdomain.com" ... )
LE_PARAM_DOMAINS=()
for domain in "${DOMAINS[@]}"; do
  LE_PARAM_DOMAINS+=("-d" "$domain")
done
LE_PARAM_DOMAINS="${LE_PARAM_DOMAINS[@]}"

# 証明書を(再)発行。
$EXEC_LE_AUTO certonly --webroot \
    --renew-by-default \
    -w /var/www/letsencrypt \
    $LE_PARAM_DOMAINS

# 現時点のサーバー証明書名リストを取得
OLD_SERVER_CERT_NAMES=$($EXEC_AWS iam list-server-certificates | jq -r ".ServerCertificateMetadataList[] | select(.ServerCertificateName | contains(\"${CERT_NAME}\")).ServerCertificateName")

# 新しい証明書のサーバー証明書名
NEW_SERVER_CERT_NAME="${CERT_NAME}-${DATE_CURRENT_YMD}"

# 新しい証明書を IAM にアップロード
$EXEC_AWS iam upload-server-certificate --server-certificate-name $NEW_SERVER_CERT_NAME \
  --certificate-body file://$CERT_PATH \
  --private-key file://$PRIVKEY_PATH \
  --certificate-chain file://$CHAIN_PATH

# 反映を待つ
sleep 15

# 新しい証明書の ARN を取得
SERVER_CERT_ARN=$($EXEC_AWS iam list-server-certificates | jq -r ".ServerCertificateMetadataList[] | select(.ServerCertificateName == \"${NEW_SERVER_CERT_NAME}\").Arn")

# ELB に新しいサーバー証明書を設定
$EXEC_AWS elb set-load-balancer-listener-ssl-certificate \
  --load-balancer-name $ELB_NAME \
  --load-balancer-port 443 \
  --ssl-certificate-id $SERVER_CERT_ARN

# 反映を待つ
sleep 15

# 古い証明書を削除
for cert_name in $OLD_SERVER_CERT_NAMES; do
  $EXEC_AWS iam delete-server-certificate --server-certificate-name $cert_name
done

改めて見なおしてみると

  • その sleep は何なんだ!?2
  • iam upload-server-certificate のレスポンスに ARN 入ってるよ!
  • サブドメインは変数にして for で連結したほうが… 直しました

等々、いろいろあるかと思いますが許してください。
また、スクリプト内で jq を使っているので、入ってない場合はインストールしておきます。

これを適当に cron で仕掛けときます。

crontab
# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed

# 毎月1日 04:00
0 4 1 * * root /root/cronjob/elb_update_cert.sh | logger -t letsencrypt -p local0.info

初回は sudo /root/cronjob/elb_update_cert.sh などとして実行できればOKです。

注意事項

Let's Encrypt は、 5 renew / 7 days の制限があります。
上のバッチを何度も叩くと簡単に達するので、テストの際は気をつけて下さい。

参考リンク

公式リンク

その他


以上です。

脚注


  1. CentOS や Debian 系でも基本的な作業内容は変わらないと思いますので Let's Encrypt 関連の環境差異だけ適宜読み替えて下さい。 aws-cli が必要です。 

  2. アップロードやリスナーへのセットを行った際のレスポンスに ARN が入ってきたりはするんですが、実際は10秒程度待たないと上手く行きませんでした。余裕を見て15秒待っています。