GAミント至上主義

Web Monomaniacal Developer.

GAEのSSL証明書更新をLet’s Encryptで完全自動化する

追記 2017/7/4 githubにうp
github.com


先週は会社の社長賞研修でサンフランシスコ、シリコンバレーに行ってきて書きたいことは山ほどあるけど、忘れてしまいそうな作業メモ。

研修中に証明書の期限が切れてしまい、リモートで更新するのが非常にストレスだったので、SSL証明書を自動化したい。


以前も考えたけど、GAEの証明書はまだgcloudコマンドからは変更できなかったので諦めた。

調べなおしてみると、2017/6/14のgcloudのアップデートで、コマンドラインからGAE用のSSL証明書をアップできるようになって、実現できるようになったので早速つくってみる。



とりあえずコマンド一発で
TOKENの作成
DNSの変更
AppEngineの証明書をアップデート
まで動いた。

あとcronで回して、念のため有効期限の監視も付ければ完璧?

監視は、デフォルトでは期限まで30日以上あったら更新されないので、期限が30日を切ったタイミングでアラート出せば何らかの原因で止まっているのが確認できるはず。


証明書にはLet's Encrypt
letsencrypt.org

やり取りは、おなじみの↓DNS認証ができるdehydratedを使う。
GitHub - lukas2511/dehydrated: letsencrypt/acme client implemented as a shell-script – just add water

前提として使うドメインはCloud DNSに入れておく必要がある。また、実行ユーザーはGoogle Cloud SDKをインストールして、最新のgcloudコマンドを使えるようにしておく。GAE使ってる環境と同じであれば大丈夫だとは思うけど。

元になるドメイン(bizocean.jp)は、AWS Route53で管理していたけど、今回使うサブドメイン(s.bizocean.jp)のNSレコードだけCloud DNSのものに向けておけば、問題なく動いた。

オリジナルのhook.shをベースに作る。とりあえず変更が必要そうなのは頭にまとめた。


gae_hook.sh

#!/usr/bin/env bash
DNS_PROJECT="oceanus-dev"
GAE_PROJECT="oceanus-gae"
CERT_ID=`gcloud beta app --project $GAE_PROJECT ssl-certificates list | awk 'NR==2 {print $1}'`
CERT_NAME="letsencrypt-auto`date "+%Y%m%d"`"
TARGET_DOMAIN="s.bizocean.jp"
ZONE_NAME="s-bizocean-jp"
ACME_TTL=60
SLEEP_SECOND=60

function deploy_challenge {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

    if [ $DOMAIN = $TARGET_DOMAIN ];then
      echo "Set TXT record of _acme-challenge.$DOMAIN to $TOKEN_VALUE"
      echo "dns update start"
      gcloud dns --project=$DNS_PROJECT record-sets transaction start -z=${ZONE_NAME}
      gcloud dns --project=$DNS_PROJECT record-sets transaction remove \
        `gcloud --project=$DNS_PROJECT dns record-sets list -z=${ZONE_NAME} --name="_acme-challenge.${DOMAIN}." | awk 'NR==2 {print $4}'` \
         -z=${ZONE_NAME} --name="_acme-challenge.${DOMAIN}." --type=TXT --ttl=${ACME_TTL}
      gcloud dns --project=$DNS_PROJECT record-sets transaction add $TOKEN_VALUE -z=${ZONE_NAME} --name="_acme-challenge.${DOMAIN}." --type=TXT --ttl=${ACME_TTL}
      gcloud dns --project=$DNS_PROJECT record-sets transaction execute -z=${ZONE_NAME}
      echo "dns update end sleep ${SLEEP_SECOND}"
      sleep $SLEEP_SECOND
    else
      echo "Don't match $TARGET_DOMAIN and $DOMAIN"
      echo "Set TXT record of _acme-challenge.$DOMAIN to $TOKEN_VALUE manually"
      read
    fi
}

function clean_challenge {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
}

function deploy_cert {
    local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
    if [ $DOMAIN = $TARGET_DOMAIN ];then
      echo "update ssl cert start"
      gcloud beta app --project ${GAE_PROJECT} ssl-certificates update $CERT_ID \
        --display-name=$CERT_NAME \
        --certificate=$FULLCHAINFILE \
        --private-key=$KEYFILE
    else
      echo "Don't match $TARGET_DOMAIN and $DOMAIN"
    fi
}

function unchanged_cert {
    local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"

}

HANDLER=$1; shift; $HANDLER $@

全体的にひたすらgcloudコマンドを並べただけで汚い。

更新後のsleepは10秒程度だと安定しなかったので、長めに60秒とした。TTLと合わせてもっと短くできるかもしれない。もしくはステータスを確認してからやるか。

ドメインチェック以外、例外処理もやってないが、それは死活監視の方に任せるつもり。


次に必要な設定。
鍵の長さはデフォが4096、GAEは2048以下しか対応していないのでアップ時に怒られる。
実行するディレクトリにconfigというそのままの名前のファイルを作成して変更する。

config

KEYSIZE="2048"

実行コマンド。-xを追加すれば残り期限にかかわらず強制定期にアップデートする。

./dehydrated -c -d s.bizocean.jp --challenge dns-01 -k ./gae_hook.sh

こんな感じで実行されればおk。
途中でこけるとトランザクションが残っちゃうので実行前にabortした方がいいかもしれない。

# INFO: Using main config file /home/yu_yamazaki/letsencrypt/dehydrated/config
Processing s.bizocean.jp
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Oct  2 00:26:00 2017 GMT (Longer than 30 days). Ignoring because renew was forced!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for s.bizocean.jp...
Set TXT record of _acme-challenge.s.bizocean.jp to RsMwB34gCgdfmks-2LGpsH0MVpBZVYkdveANiAcTXlk
dns update start
ERROR: (gcloud.dns.record-sets.transaction.start) transaction already exists at [transaction.yaml]
Record removal appended to transaction at [transaction.yaml].
Record addition appended to transaction at [transaction.yaml].
Executed transaction [transaction.yaml] for managed-zone [s-bizocean-jp].
Created [https://www.googleapis.com/dns/v1/projects/oceanus-dev/managedZones/s-bizocean-jp/changes/21].
ID  START_TIME                STATUS
21  2017-07-04T01:44:46.816Z  pending
dns update end sleep 60
  + Responding to challenge for s.bizocean.jp...
 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Done!

期限内だったら何もせずに終了する

% ./dehydrated -c -d s.bizocean.jp --challenge dns-01 -k ./gae_hook.sh
# INFO: Using main config file /home/BIZOCEAN/yu_yamazaki/letsencrypt/dehydrated/config
Processing s.bizocean.jp
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Oct  2 00:46:00 2017 GMT (Longer than 30 days). Skipping renew!


今度はHTTPSロードバランサーの方も自動化したい

プログラミング Google App Engine

プログラミング Google App Engine