CertbotでCloudflare登録ドメインの証明書を発行する

Certbot(Let's encrypt) を使って Cloudflare で管理しているドメインのワイルドカード証明書を DNS-01 方式で発行する手順を、毎度調べなくていいようにまとめておきます。

Cloudflare APIキー

My Profile | Cloudflare から Global API Key を取得します。

環境変数

使用する環境変数は .env にまとめておきます。

.env
# Cloudflare
DNS_CLOUDFLARE_EMAIL=email@example.com
DNS_CLOUDFLARE_API_KEY=a1b2c3d4e5
CLOUDFLARE_CREDENTIALS=/root/.cloudflare/credentials

# Certbot
CERTBOT_DOMAIN=example.com
## 本番(テスト以外)は空文字にする
CERTBOT_TEST=--test-cert
  • DNS_CLOUDFLARE_EMAIL: Cloudflare アカウントのメールアドレス
  • DNS_CLOUDFLARE_API_KEY: Cloudflare の `Global API Key
  • CLOUDFLARE_CREDENTIALS: Certbot プラグインが使用する情報が書かれたファイルのパス
  • CERTBOT_DOMAIN: (ワイルドカード)証明書を発行するドメイン
  • CERTBOT_TEST: テスト用の証明書を発行するかどうか

    • 本番: 空文字
    • テスト: 何かしらの文字

Certbot 準備

Certbot の Dockerfile を作ります。docker-compose の args から渡される DNS_CLOUDFLARE_EMAIL, DNS_CLOUDFLARE_API_KEY を credential ファイルに出力します。

/certbot/Dockerfile
FROM certbot/dns-cloudflare

ARG DNS_CLOUDFLARE_EMAIL
ARG DNS_CLOUDFLARE_API_KEY
ARG CLOUDFLARE_CREDENTIALS
ARG CERTBOT_TEST
ARG CERTBOT_DOMAIN

# certbot credential file
RUN \
  mkdir -p /root/.cloudflare/ && \
  echo "dns_cloudflare_email = ${DNS_CLOUDFLARE_EMAIL}" > ${CLOUDFLARE_CREDENTIALS} && \
  echo "dns_cloudflare_api_key = ${DNS_CLOUDFLARE_API_KEY}" >> ${CLOUDFLARE_CREDENTIALS} && \
  chmod 700 ${CLOUDFLARE_CREDENTIALS}

# entrypoint
COPY entrypoint.sh /entrypoint.sh

RUN chmod 500 /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

エントリーポイントには entrypoint.sh を設定します。

Certbot コマンドによる証明書の発行・再発行ができるように、entrypoint.sh の第 1 引数が certonly のときは certbot certonly [opts] が、renew のときは certbot renew [opts] が実行されるようにします。

certbot/entrypoint.sh
#!/bin/sh

set -euC

error () {
  err="ERROR"
  message=": ${1}"
  echo -e "\033[0;31m${err}\033[0;39m${message}" >&2
  exit 1
}

# certbot command
command=""
add_command () {
  command="$1 ${command}"
}
add_command_arg () {
  command="${command} $1"
}

# debug mode
if [ -n "${CERTBOT_TEST}" ]; then
  add_command_arg "--test-cert"
fi

# subcommand
sub_command=$1

if [ ${sub_command} = "certonly" ]; then
  # certonly command
  add_command ${sub_command}

  # dns-cloudflare
  if [ -n "${CLOUDFLARE_CREDENTIALS}" ]; then
    add_command_arg "--dns-cloudflare"
    add_command_arg "--dns-cloudflare-credentials ${CLOUDFLARE_CREDENTIALS}"
  fi

  # email
  if [ -n "${DNS_CLOUDFLARE_EMAIL}" ]; then
    add_command_arg "--email ${DNS_CLOUDFLARE_EMAIL}"
  fi

  # other opts
  add_command_arg "--agree-tos"
  add_command_arg "--non-interactive"

  # domain
  if [ -n "${CERTBOT_DOMAIN}" ]; then
    add_command_arg "-d ${CERTBOT_DOMAIN}"
    add_command_arg "-d *.${CERTBOT_DOMAIN}"
  fi

elif [ ${sub_command} = "renew" ]; then
  # renew command
  add_command ${sub_command}

else
  error "Invalid subcommand: ${sub_command}"
fi

# run certbot
eval "certbot ${command}"

Nginx 準備

Nginx の Dockerfile も作ります。

nginx/Dockerfile
FROM nginx:1.17.8-alpine as base

RUN rm /etc/nginx/conf.d/*

##### production #####
FROM base as production

COPY ./default.conf /etc/nginx/conf.d/

CMD [ "nginx", "-g", "daemon off;" ]
nginx/default.conf
# upstream appserver {
#   server app:3000;
# }

server {
  listen 80 default_server;

  root /usr/share/nginx/html;
  index  index.html;

  # nginx version
  server_tokens off;

  # security header
  add_header x-xss-orotection "1; mode=block";
  add_header x-content-type-options nosniff;
  add_header x-frame-options DENY;
  add_header x-download-options noopen;

  location / {
    return 301 https://$host$request_uri;
  }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  root /usr/share/nginx/html;
  index  index.html;

  # SSL Settings
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_ciphers 'ECDH !aNULL !eNULL !SSLv2 !SSLv3';

  # rename "example.com" to my domain
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  # Nginx version
  server_tokens off;

  # Security header
  add_header x-xss-orotection "1; mode=block";
  add_header x-content-type-options nosniff;
  add_header x-frame-options DENY;
  add_header x-download-options noopen;

  # HTTP Strict Transport Security
  add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload';

  # location / {
  #   proxy_pass http://appserver;
  # }
}

docker-compose にまとめる

上記で用意した設定ファイルを docker-compose にまとめます。

docker-compose.yml
version: '3.4'

services:
  nginx:
    build:
      context: ./nginx
      dockerfile: ./Dockerfile
      target: production
    ports:
      - 80:80
      - 443:443
    volumes:
      - letsencrypt:/etc/letsencrypt
    restart: unless-stopped

  certbot:
    build:
      context: ./certbot
      dockerfile: ./Dockerfile
      args:
        - DNS_CLOUDFLARE_EMAIL=$DNS_CLOUDFLARE_EMAIL
        - DNS_CLOUDFLARE_API_KEY=$DNS_CLOUDFLARE_API_KEY
        - CLOUDFLARE_CREDENTIALS=$CLOUDFLARE_CREDENTIALS
        - CERTBOT_TEST=$CERTBOT_TEST
    env_file: .env
    volumes:
      - letsencrypt:/etc/letsencrypt

volumes:
  letsencrypt:

証明書を発行する

証明書を発行するドメインなどの情報は .env に記入されているものとします。

はじめに docker-compose build でイメージの作成を行います。

$ docker-compose build
...
Successfully tagged certbot-cloudflare_nginx:latest
...
Successfully tagged certbot-cloudflare_certbot:latest

初回の証明書

証明書発行の初回は Certbot の certonly コマンドを実行する必要があります。

entrypoint.sh にて certbot certonly を実行したいため、ここでは docker-compose run certbot certonly コマンドを入力します。

$ docker-compose run certbot certonly
Creating network "certbot-cloudflare_default" with the default driver
Creating volume "certbot-cloudflare_letsencrypt" with default driver
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator dns-cloudflare, Installer None
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for example.com
dns-01 challenge for example.com
Waiting 10 seconds for DNS changes to propagate
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/example.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/example.com/privkey.pem
   Your cert will expire on 2020-10-13. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.

これで証明書が発行されました。

証明書の更新

証明書の更新には certbot renew コマンドを実行する必要があるため、ここでは docker-compose run certbot renew を入力します。

$ docker-compose run certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not yet due for renewal

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

The following certs are not due for renewal yet:
  /etc/letsencrypt/live/example.com/fullchain.pem expires on 2020-10-13 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

certbot renew は証明書の有効期限が 30 日未満のときのみ証明書を再発行するため、今回は何も行われませんでした。

Nginxの起動

発行した証明書は volumes で共通しているため、nginx コンテナでも証明書を読み込むことできます。

$ docker-compose up -d nginx
Creating certbot-cloudflare_nginx_1 ... done

おまけのログ確認

Certbot から発行された有効な証明書の履歴は crt.sh で手軽に確認できます。

参考