Cloudflare DNS + Docker + Let's Encrypt + Nginx で TLS->TLS なプロキシを作る

要するに次のことを行います。

  • Nginx で TLS->TLS なプロキシを作る
    • TLS は Nginx でいったん切る
  • 証明書は Let's Encrypt で取得する
    • Nginx は http はリッスンさせないので DNS 認証を使う
  • ドメインは Cloudflare で管理している
    • ゾーンの委任やレコードの登録は既に終わっている前提

次のような docker-compose.yml を用意します。

version: "3.7"

services:
  nginx:
    image: nginx:alpine
    ports:
      - 9999:9999
    pid: service:letsencrypt
    volumes:
      - letsencrypt:/etc/letsencrypt/
      - run:/run/
    environment:
      NGINX_CONFIG: |
        daemon off;
        worker_processes auto;
        error_log /dev/stderr notice;
        pid /run/nginx.pid;
        events {
            worker_connections 32;
        }
        stream {
            error_log /dev/stderr info;
            upstream backend {
                server 192.0.2.123:9999;
            }
            server {
                listen 9999 ssl;
                proxy_pass backend;
                proxy_ssl on;
                ssl_certificate /etc/letsencrypt/live/ore.example.com/fullchain.pem;
                ssl_certificate_key /etc/letsencrypt/live/ore.example.com/privkey.pem;
                ssl_session_timeout 1d;
                ssl_session_cache shared:MozSSL:10m;
                ssl_session_tickets off;
                ssl_protocols TLSv1.3;
                ssl_prefer_server_ciphers off;
            }
        }
    command:
      - sh
      - -euc
      - |
        echo "$$NGINX_CONFIG" > /root/nginx.conf
        exec nginx -c /root/nginx.conf

  letsencrypt:
    image: certbot/dns-cloudflare
    volumes:
      - letsencrypt:/etc/letsencrypt/
      - run:/run/
    environment:
      DNS_CLOUDFLARE_EMAIL: ~
      DNS_CLOUDFLARE_API_KEY: ~
    entrypoint:
      - sh
      - -euc
      - |
        mkdir -p /root/.cloudflare/
        > /root/.cloudflare/credentials
        chmod 700 /root/.cloudflare/
        chmod 600 /root/.cloudflare/credentials
        echo dns_cloudflare_email   = "$$DNS_CLOUDFLARE_EMAIL"   >> /root/.cloudflare/credentials
        echo dns_cloudflare_api_key = "$$DNS_CLOUDFLARE_API_KEY" >> /root/.cloudflare/credentials
        exec "$$@"
      - --
    command:
      - sh
      - -euc
      - |
        while :; do
          certbot renew -n --agree-tos --keep --post-hook='kill -HUP "$$(cat /run/nginx.pid)"'
          sleep 86400
        done

volumes:
  letsencrypt:
  run:

要点を記載します。

  • nginx で TLS->TLS なプロキシをします
    • stream の listen の ssl と proxy_ssl
  • サーバ証明書のドメイン認証には DNS 認証を使います
    • DNS は CloudFlare に委任しており certbot の dns-cloudflare で DNS 認証できます
  • 証明書更新時に nginx に HUP を撃つ必要があるので・・・
    • pid: service:letsencrypt で PID 名前空間を共用します
    • nginx の pid ファイルのディレクトリをボリュームで共有します
  • nginx.conf は環境変数で渡して実行時にファイルに書き出し
    • Docker Remote API でデプロイする想定なのでホストのファイルのマウントはやりたくない
  • CloudFlare のAPIキーも環境変数で渡します
    • ENTRYPOINT でファイルに書き込みます
    • docker-compose run したときでも使えるように

まずは次のように実行して証明書を取得します。

docker-compose run --rm letsencrypt \
  certbot certonly -n --agree-tos \
    --test-cert \
    --dns-cloudflare \
    --dns-cloudflare-credentials /root/.cloudflare/credentials \
    --dns-cloudflare-propagation-seconds 60 \
    -m 'ore@example.com' \
    -d 'ore.example.com'

サービスを開始します。

docker-compose up -d
docker-compose ps
docker-compose logs -f

動作確認します。

openssl s_client -connect ore.example.com:9999 </dev/null | openssl x509 -noout -text

さいごに

とあるニッチな理由で TLS->TLS なプロキシが欲しくてやったのだけど、よく考えると使わなさそうでお蔵入りしそうなので備忘的にポスト。