Ansible x ACMEモジュールを利用したSSL証明書の自動更新

本記事は Ansible Advent Calendar 2025 の11日目の記事です。

はじめに

昨今、セキュリティに対する関心が昔以上に高まっており、様々な領域でセキュリティを強化する流れになっています。その一つとして、「SSL証明書の有効期間を短くする」というものがあります。「証明書の47日問題」という単語を聞いた方もいると思います。

TLS 証明書の有効期間は 47 日へ短縮されることが決定

これは業界全体のセキュリティ強化という点では歓迎すべきですが、運用する観点から見ると単純に証明書の更新作業の回数が増えるということになり、運用の負荷が上がります。

証明書業界ではこの運用負荷を軽減するための仕組みを提供しており、それが Automated Certificate Management Environment(自動証明書管理環境) という自動手続きの仕組みです。これをサポートした Ansible のモジュール community.crypto.acme_certificate があり、これを使うと前後の処理を含めて SSL証明書の更新を完全に自動化することができます。

今回はその方法について見ていきましょう。

証明書更新の流れ

SSLの証明書の発行はざっくりと以下の流れで進みます。

  1. 証明書ベンダーに証明書の発行・更新要求を行う
  2. ドメイン所有権の確認
  3. 証明書の発行

ACEM ではこの流れを自動化するための仕組みを提供しています。ACME での手続きは以下のようになります。

  1. 証明書ベンダーの ACME API に csr を送信する
  2. ACME からドメイン所有権を確認する手順が返信される
  3. 所有権を証明する手順を実行
  4. ACME API に手順が完了した旨を通知
  5. ACME が対応を確認し、ドメイン所有者であることを確認
  6. 証明書が発行

この流れの 2-4 がポイントで、ACMEの中でドメイン所有権を証明する「チャレンジ」と呼ばれる工程です。

このチャレンジにはいくつかの方法があり、Let’s Encrypt のサイト で詳しく紹介されています(どのチャレンジ方法に対応しているかは証明書ベンダーによる)。

この中では HTTP-01 が最も一般的で簡単な方法になります。

次に、community.crypto.acme_certificate モジュール を使ってどのように証明書を自動化するのかを見ていきましょう。

証明書更新 Playbook

この Playbook では Let’s Encrpyt の ACME を使って nginx 用の ssl 証明書を発行しています。実際の流れを見ていきましょう。ここではポイントのみ抜粋します。

まずCSRを作成したら、ACMEモジュールを使って証明書の発行リクエストを行います。

    - name: Get challenge data for {{ server_fqdn }} from Let's Encrpyt
      community.crypto.acme_certificate:
        account_key_src: "/etc/pki/tls/certs/{{ server_fqdn }}/account.key"
        account_email: "{{ admin_email }}"
        csr: "/etc/pki/tls/certs/{{ server_fqdn }}/{{ server_fqdn }}.csr"
        dest: "/etc/pki/tls/certs/{{ server_fqdn }}/{{ server_fqdn }}.crt"
        acme_directory: https://acme-v02.api.letsencrypt.org/directory
        acme_version: 2
        challenge: http-01
        terms_agreed: true
        remaining_days: 30
      register: ret_acme_challenge

このリクエストを行うと、公式ドキュメント にあるレスポンスがそのまま返ってきます。ここにチャレンジを行うための情報が含まれています。

http-01の場合はこんな感じです(抜粋)

ret_acme_challenge:
  challenge_data:
    challenges:
      http-01:
        resource: .well-known/acme-challenge/xxxyyyzzzzz
        resource_value: aaaaabbbbccccddddeeeee

これがチャレンジ(ドメイン所有権の証明)を行うための材料になります。具体的には、上記のデータを自分のサイト(SSLを使いたいサイト)に配置し、そこに証明書ベンダーがアクセスしてデータを確認できれば「所有権あり(ドメイン所有者が証明書発行をリクエストしている)」と判断されます。

このデータを使って、自分のサイトにチャレンジデータを配置する例が以下になります(ファイルをおいているだけです)

    - when:
        - ret_acme_challenge is changed
        - server_fqdn in ret_acme_challenge['challenge_data']
      block:
        - name: create challenge path
          ansible.builtin.file:
            state: directory
            path: /var/www/letsencrypt/{{ ret_acme_challenge['challenge_data'][server_fqdn]['http-01']['resource'] | dirname }}

        - name: put challenge data
          ansible.builtin.copy:
            dest: /var/www/letsencrypt/{{ ret_acme_challenge['challenge_data'][server_fqdn]['http-01']['resource'] }}
            content: |
              {{ ret_acme_challenge['challenge_data'][server_fqdn]['http-01']['resource_value'] }}

        - name: nginx conf for challege data
          ansible.builtin.copy:
            dest: /etc/nginx/default.d/letsenc_challenge.conf
            content: |
              location ^~ /.well-known/acme-challenge/ {
                default_type "text/plain";
                root /var/www/letsencrypt;
              }

        - name: reload nginx conf
          ansible.builtin.systemd_service:
            name: nginx
            state: reloaded

これで http://<server_fqdn>/.well-known/acme-challenge/xxxyyyzzzzz にアクセスすると、resource_value の値が見られるようになります。

http-01 では http のアクセスが行われるので、FWやセキュリティグループでポート80へのアクセスが許可されている必要があります。これが難しい場合には DNS を使った dns-01 等の別の方法でチャレンジすることになります。

準備ができたら ACME にチャレンジ要求を行います。data 部分に更新要求したときの戻り値(ret_acme_challenge) をそのまま付けてリクエストを送信します。

        - name: Validate challenge and save certs
          community.crypto.acme_certificate:
            acme_directory: https://acme-v02.api.letsencrypt.org/directory
            acme_version: 2
            terms_agreed: true
            account_key_src: "/etc/pki/tls/certs/{{ server_fqdn }}/account.key"
            csr: "/etc/pki/tls/certs/{{ server_fqdn }}/{{ server_fqdn }}.csr"
            dest: "/etc/pki/tls/certs/{{ server_fqdn }}/{{ server_fqdn }}.crt"
            data: "{{ ret_acme_challenge }}"

チャレンジを要求すると、証明書ベンダーが配置したチャレンジデータを確認し、成功すると証明書が発行されて、dest に保存されます。

# ls -alF /etc/pki/tls/certs/aitac.emacs-lisp.net/
total 28
drwxr-xr-x. 2 root root  121 Dec  8 05:35 ./
drwxr-xr-x. 3 root root 8192 Dec  8 04:02 ../
-rw-------. 1 root root 3243 Dec  8 04:03 account.key
-rw-r--r--. 1 root root 2159 Dec  8 05:35 aitac.emacs-lisp.net.crt
-rw-r--r--. 1 root root 1667 Dec  8 04:04 aitac.emacs-lisp.net.csr
-rw-------. 1 root root 3243 Dec  8 04:03 aitac.emacs-lisp.net.key

証明書の更新

community.crypto.acme_certificate では、パラメーター remaining_days: 30 で「証明書の期限が残り何日以上で更新する」というオプションが指定できます。

サンプルのように、ここに 30 と指定しておくと、最初のリクエスを送った際に、

という挙動になります。この変化を when や handler でキャッチして挙動を操作することができます。

Ansible で SSL 証明書更新する場面

既にACMEに対応している証明書ベンダーでは、サーバー用の更新スクリプト(有名どころは certbot) を提供しており、わざわざ Ansible で Playbook を書かなくてもスクリプト一発で更新できます。この手のスクリプトはリクエストからチャレンジまでを自動でやってくれます。

しかし、SSLの終端を専用のロードバランサやリバースプロキシで実施している場合にはこれらのスクリプトは利用出来ません。こういった場合に Ansible を用いることで自動化が可能となります。また、証明書の更新前後でメンテナンスページを表示するといった、何かしらの前後処理を実行したい場合などにも Ansible は活躍してくれます。

おわりに

今回は SSL証明書の更新を Ansible を使って自動化する例について紹介しました。Ansible は自動化できる範囲が広いため、今回の証明書更新でも様々な応用が可能となります。例えば、 uri モジュールを使って Web サーバーにアクセスすることで、証明書の有効期限を一般ユーザーと同じ経路から確認することもできます。これを利用すると、

  1. 一般ユーザーと同じ経路で https アクセスを行い、証明書の有効期限を確認する
  2. 有効期限が N日以下なら実際に更新を行う
  3. この自動化を1日1回流す

というような、監視の役割を兼ねた証明書更新の自動化を実現することも可能となります。

どんどん応用してAnsibleの可能性を追求していきましょう。

参考サイト