TL;DR
- Let's Encrypt で証明書発行して
- IAM にアップロードして
- ELB に設定して
- 古い証明書を削除する
という手順を実施する Bash スクリプト (root 実行用)
https://gist.github.com/hidekuro/1801e711367b47a7e519
本稿ではこれを使うまでの道のりをダラダラと説明しています。
はじめに
AWS ELB に Let's Encrypt の証明書を登録する仕組み を自動化したので、
その作業やバッチスクリプトのソースを残しておきます。
※ 実稼働させたスクリプトから秘匿情報を間引きつつ記事に起こしていますので、不備があればご指摘下さい。
要件
- ELB に登録するサーバー証明書の発行と更新作業を自動化したい
- 稼働中の Web サーバーは止めたくない
前提環境
- SSH で作業可能で、 Nginx が稼動している(稼働させる) EC2 インスタンスを持っている。
- EC2 インスタンスの OS は AmazonLinux とする。1
- 独自ドメインを所持しており、 ELB の CNAME が引ける状態になっている。
おおまかな流れ
- Let's Encrypt Client を使う準備をする。
- Let's Encrypt が提供するコマンドで証明書を発行する。
- aws-cli で証明書を IAM にアップロードする。
- アップロードした証明書を 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 する設定を書いている場合などは、下のようにして穴を開けてやる必要があります。
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
#!/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 で仕掛けときます。
# 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 の制限があります。
上のバッチを何度も叩くと簡単に達するので、テストの際は気をつけて下さい。
参考リンク
公式リンク
その他
-
Letsencrypt-auto openssl module not found EC2
- AmazonLinux で OpenSSL module がどうのこうのというトラブル事例
-
alex/letsencrypt-aws
- ELB投入ツールをガチで開発されている方もいます
以上です。