AnsibleでSSL証明書の更新と配布を自動化する設計入門|community.cryptoモジュールで期限監視から鍵管理まで

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > Ansible > AnsibleでSSL証明書の更新と配布を自動化する設計入門|community.cryptoモジュールで期限監視から鍵管理まで
現場で「SSL証明書の期限が切れてサービスが止まった」という事故は今でも起きる。
証明書の有効期間は1年であることが多く、年に1度の更新作業は忘れやすい。サーバーが10台・20台と増えると、どのサーバーの証明書がいつ期限を迎えるかをスプレッドシートで管理するのは現実的でなくなる。

Ansibleのcommunity.cryptoコレクションを使えば、秘密鍵の生成・CSR作成・証明書の配布・期限確認を一つのroleで管理できる。「証明書の更新を人が覚えておく」という属人的な運用から抜け出せる。

この記事では、community.cryptoの証明書関連モジュールを活用した設計パターンを解説する。自己署名証明書の生成roleから、既存証明書の期限確認と自動更新、Let's Encryptとの連携まで、設計の考え方を中心にカバーする。Ansible VaultやRoleの基礎から体系的に習得したい方にはAnsibleの実践ハンズオン講座もぜひ参照してほしい。

この記事のポイント

・community.cryptoコレクションで秘密鍵・CSR・証明書の全工程をAnsible管理できる
・x509_certificate_infoで期限残日数を取得し、条件分岐で自動更新を設計できる
・Ansible Vaultと組み合わせて秘密鍵をリポジトリに安全に格納できる
・Let's Encrypt連携もacme_certificateモジュールで一つのPlaybookに統合できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

SSL証明書を手動管理することのリスク

証明書管理の典型的な失敗パターンが2つある。

一つ目は「更新を忘れて期限切れ」。証明書の更新作業がカレンダーの予定として個人の頭の中にしかなく、担当者の異動や休暇で見逃されるケースだ。Let's Encryptが無料で証明書を発行するようになってから「証明書のコストは0円」になったが、「更新の手間が0」にはなっていない。certbotのcronを設定しているだけで自動更新できるはずが、cronの環境変数が不足していて静かに失敗していた——という現場は珍しくない。

二つ目は「どのサーバーに何の証明書が入っているかわからない」。10台を超えると、各サーバーに個別にSSHして openssl x509 -in /etc/ssl/certs/server.crt -noout -dates を叩いて確認するのは非現実的になる。

Ansibleでこれを解決するアプローチは2つを組み合わせる。「証明書をAnsibleが生成・配布・管理する」構成と、「Ansibleが既存証明書の期限を一元監視する」構成だ。

community.cryptoで使える証明書関連モジュールの全体像

証明書管理に使うモジュールはすべてcommunity.cryptoコレクションに含まれる。まずコレクションのインストールを確認する。

# community.cryptoコレクションのインストール $ ansible-galaxy collection install community.crypto Starting galaxy collection install process Process install dependency map Starting collection install process Downloading https://galaxy.ansible.com/download/community-crypto-2.21.1.tar.gz Installing 'community.crypto:2.21.1' to '~/.ansible/collections/ansible_collections/community/crypto' community.crypto:2.21.1 was installed successfully

主要モジュールは5つだ。

・openssl_privatekey:秘密鍵の生成(RSA/EC両対応)
・openssl_csr:CSR(Certificate Signing Request)の生成
・x509_certificate:証明書の生成。自己署名・CAサインなど複数の provider を選べる
・x509_certificate_info:証明書のメタデータ(期限・SAN・発行者など)をJSON形式で取得
・acme_certificate:ACME v2プロトコル経由でLet's Encryptなどから証明書を取得

Ansible 2.10以降はすべてFQCN(完全修飾コレクション名)で呼び出すのが正式な書き方だ。短名(openssl_certificate)は引き続き動作するが、将来的に廃止予定のため新規Playbookでは community.crypto.x509_certificate と書くことを推奨する。

秘密鍵とCSRを生成するroleの基本設計

自己署名証明書を生成して複数のWebサーバーに配布するroleの構成から始める。ディレクトリ構造は以下のとおりだ。

roles/ssl_cert/ ├── defaults/ │ └── main.yml # 証明書の有効期間・鍵長などのデフォルト値 ├── handlers/ │ └── main.yml # Webサーバーのreloadハンドラー ├── tasks/ │ ├── main.yml # タスクのエントリーポイント │ ├── generate.yml # 秘密鍵・CSR・証明書の生成 │ └── deploy.yml # 配布とサービス再起動 └── files/ └── (暗号化済みの秘密鍵ファイルをVaultで格納する場合はここに置く)

defaults/main.ymlでデフォルト値を定義する。roleを呼び出す側のgroup_varsやhost_varsで上書きできる。

# roles/ssl_cert/defaults/main.yml ssl_cert_key_size: 4096 ssl_cert_key_type: "RSA" ssl_cert_country: "JP" ssl_cert_org: "Example Corp" ssl_cert_validity_days: 365 ssl_cert_tmp_dir: "/tmp/ansible_ssl" ssl_cert_key_dir: "/etc/ssl/private" ssl_cert_crt_dir: "/etc/ssl/certs" ssl_cert_service_name: "nginx"

tasks/generate.ymlで秘密鍵からCSR・証明書の生成まで連続したタスクを定義する。delegate_to: localhost でコントローラーノード上で証明書を生成し、各サーバーに配布する設計にすることで、管理対象ノードにOpenSSLのツールチェーンがインストールされていなくても動作する。

# roles/ssl_cert/tasks/generate.yml --- - name: 作業ディレクトリを確保する ansible.builtin.file: path: "{{ ssl_cert_tmp_dir }}" state: directory mode: "0700" delegate_to: localhost - name: 秘密鍵を生成する community.crypto.openssl_privatekey: path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" size: "{{ ssl_cert_key_size }}" type: "{{ ssl_cert_key_type }}" mode: "0600" delegate_to: localhost - name: CSRを生成する community.crypto.openssl_csr: path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.csr" privatekey_path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" common_name: "{{ ansible_fqdn }}" country_name: "{{ ssl_cert_country }}" organization_name: "{{ ssl_cert_org }}" subject_alt_name: - "DNS:{{ inventory_hostname }}" - "DNS:{{ ansible_fqdn }}" delegate_to: localhost - name: 自己署名証明書を生成する community.crypto.x509_certificate: path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.crt" privatekey_path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" csr_path: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.csr" provider: selfsigned selfsigned_not_after: "+{{ ssl_cert_validity_days }}d" mode: "0644" delegate_to: localhost

tasks/deploy.ymlで生成した証明書を各ノードに配布し、Webサーバーのreloadをnotifyする。

# roles/ssl_cert/tasks/deploy.yml --- - name: 秘密鍵を配布する ansible.builtin.copy: src: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.key" dest: "{{ ssl_cert_key_dir }}/{{ inventory_hostname }}.key" owner: root group: root mode: "0600" notify: reload webserver - name: 証明書を配布する ansible.builtin.copy: src: "{{ ssl_cert_tmp_dir }}/{{ inventory_hostname }}.crt" dest: "{{ ssl_cert_crt_dir }}/{{ inventory_hostname }}.crt" owner: root group: root mode: "0644" notify: reload webserver

handlers/main.ymlにWebサーバーのreloadハンドラーを書く。

# roles/ssl_cert/handlers/main.yml --- - name: reload webserver ansible.builtin.service: name: "{{ ssl_cert_service_name }}" state: reloaded

証明書の期限確認を自動化するタスク設計

証明書を配布した後は「期限の残り日数を定期チェックする」仕組みが必要だ。x509_certificate_info モジュールで証明書のメタデータを取得し、残り日数が閾値を下回ったら更新フローを起動する設計にする。

# tasks/check_expiry.yml --- - name: 証明書の有効期限情報を取得する community.crypto.x509_certificate_info: path: "{{ ssl_cert_crt_dir }}/{{ inventory_hostname }}.crt" register: cert_info - name: 有効期限の残り日数を計算する ansible.builtin.set_fact: cert_days_remaining: >- {{ ((cert_info.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ'))).days }} - name: 期限確認の結果を表示する ansible.builtin.debug: msg: "{{ inventory_hostname }}: 証明書残り {{ cert_days_remaining }} 日(期限 {{ cert_info.not_after }})" - name: 期限が近い場合に警告を出す(30日未満) ansible.builtin.debug: msg: "【警告】{{ inventory_hostname }} の証明書は {{ cert_days_remaining }} 日で期限切れ" when: cert_days_remaining | int < 30 - name: 期限が30日未満なら自動更新する ansible.builtin.include_tasks: generate.yml when: cert_days_remaining | int < 30

RHEL 9.4の検証サーバー3台に対してこのタスクを実行した出力例:

TASK [証明書の有効期限情報を取得する] **** ok: [web01.example.com] ok: [web02.example.com] ok: [web03.example.com] TASK [期限確認の結果を表示する] **** ok: [web01.example.com] => { "msg": "web01.example.com: 証明書残り 342 日(期限 20271015103022Z)" } ok: [web02.example.com] => { "msg": "web02.example.com: 証明書残り 18 日(期限 20261103143011Z)" } ok: [web03.example.com] => { "msg": "web03.example.com: 証明書残り 289 日(期限 20270822080341Z)" } TASK [期限が近い場合に警告を出す(30日未満)] **** skipping: [web01.example.com] ok: [web02.example.com] => { "msg": "【警告】web02.example.com の証明書は 18 日で期限切れ" } skipping: [web03.example.com]

この例ではweb02だけが30日を切っており、generate.ymlのinclude_tasksが呼ばれて自動的に証明書が再生成・再配布される。この仕組みをAWXやAutomation Controllerのスケジュール実行に乗せれば、証明書の更新を完全に自動化できる。

Let's Encryptと組み合わせたacme_certificateの設計

自己署名証明書ではなく、Let's Encryptの無料証明書を使うケースも多い。community.cryptoのacme_certificateモジュールで、ACME認証(Let's EncryptのDV証明書取得)をPlaybookに組み込める。 HTTP-01チャレンジを使うPlaybookの骨格を示す。

# tasks/letsencrypt.yml --- - name: ACME用アカウント秘密鍵を生成する(初回のみ) community.crypto.openssl_privatekey: path: "{{ ssl_cert_tmp_dir }}/acme_account.key" size: 4096 delegate_to: localhost run_once: true - name: ドメイン用の秘密鍵を生成する community.crypto.openssl_privatekey: path: "{{ ssl_cert_tmp_dir }}/{{ item }}.key" size: 2048 loop: "{{ ssl_domains }}" delegate_to: localhost - name: CSRを生成する community.crypto.openssl_csr: path: "{{ ssl_cert_tmp_dir }}/{{ item }}.csr" privatekey_path: "{{ ssl_cert_tmp_dir }}/{{ item }}.key" common_name: "{{ item }}" loop: "{{ ssl_domains }}" delegate_to: localhost - name: ACME Challenge(HTTP-01)を開始する community.crypto.acme_certificate: account_key_src: "{{ ssl_cert_tmp_dir }}/acme_account.key" csr: "{{ ssl_cert_tmp_dir }}/{{ item }}.csr" dest: "{{ ssl_cert_tmp_dir }}/{{ item }}.crt" fullchain_dest: "{{ ssl_cert_tmp_dir }}/{{ item }}_fullchain.crt" acme_directory: "https://acme-v02.api.letsencrypt.org/directory" acme_version: 2 challenge: "http-01" remaining_days: 30 loop: "{{ ssl_domains }}" delegate_to: localhost register: acme_challenge

HTTP-01チャレンジではLet's Encryptが .well-known/acme-challenge/ 配下にHTTPで到達できることを確認する。チャレンジファイルをWebサーバーの公開ディレクトリに設置し、応答が返ったら証明書が発行される流れだ。

DNS-01チャレンジを使う場合は、DNSへのTXTレコード設定が必要になる。AWSのRoute53を使っているなら community.aws.route53 と組み合わせてDNSレコードの自動設置もPlaybookに含められる。ワイルドカード証明書(*.example.com)はHTTP-01では取得できないためDNS-01を選ぶ。

Ansible Vaultで秘密鍵をリポジトリに安全に保管する設計

証明書管理のroleをGitリポジトリで管理する場合、秘密鍵をそのまま files/ ディレクトリに置くと秘密鍵がリポジトリに残ってしまう。Ansible Vaultを使って秘密鍵ファイル自体を暗号化してリポジトリに保存するアプローチが一般的だ。

# 秘密鍵を生成した後、Vaultで暗号化する $ ansible-vault encrypt /tmp/ansible_ssl/web01.example.com.key --output roles/ssl_cert/files/web01.example.com.key.vault Encryption successful # 暗号化されたことを確認する $ head -1 roles/ssl_cert/files/web01.example.com.key.vault $ANSIBLE_VAULT;1.1;AES256

タスク側は ansible.builtin.copy の src にvaultで暗号化したファイルを指定するだけでよい。ansible-playbook実行時に --vault-password-file または --ask-vault-pass を指定すれば、Ansibleが自動的に復号して配布する。

- name: 暗号化された秘密鍵を配布する(Vaultが自動復号) ansible.builtin.copy: src: "web01.example.com.key.vault" dest: "/etc/ssl/private/web01.example.com.key" owner: root group: root mode: "0600" notify: reload webserver

プロジェクト設計の観点では、秘密鍵の扱いは次の2択になる。

・コントローラーローカルに置く設計:リポジトリには含めず、コントローラーノードのローカルファイルシステムに保持する。シンプルだが、コントローラーノードが単一障害点になるリスクがある
・Vaultで暗号化してリポジトリに含める設計:Gitで履歴管理でき、複数チームメンバーで共有できる。ただしVaultのパスワード管理が別途必要になる

チームの規模とセキュリティポリシーに合わせて選択する。CI/CD環境(GitHub ActionsやAWX)で自動実行する場合は、VaultのパスワードをCI側のSecretsに格納してPlaybook実行時に渡す構成が多い。

トラブルシュート|証明書関連のよくあるエラー

1. community.cryptoコレクションが見つからない

エラーメッセージ:couldn't resolve module/action 'community.crypto.x509_certificate'

requirements.ymlにコレクションが記載されていないか、ansible-galaxy collection install が実行されていないケースだ。

# requirements.yml にコレクションを記載する $ cat requirements.yml --- collections: - name: community.crypto version: ">=2.0.0" # インストールする $ ansible-galaxy collection install -r requirements.yml

2. Pythonのcryptographyパッケージが不足している

エラーメッセージ:You need to have the Python cryptography library (>= 1.6) installed on the remote host

community.cryptoモジュールの多くはPythonのcryptographyパッケージを必要とする。

# RHEL系(RHEL 9.4で確認) $ sudo dnf install python3-cryptography # Ubuntu系(Ubuntu 24.04 LTSで確認) $ sudo apt install python3-cryptography

3. delegate_to: localhostでPermission denied

エラーメッセージ:Permission denied: '/tmp/web01.key'

秘密鍵の生成先 /tmp/ がスティッキービット(chmod 1777)になっている場合、rootで生成したファイルを別ユーザーが読めないことがある。defaults/main.ymlの ssl_cert_tmp_dir をansibleユーザーがアクセスできる専用ディレクトリに変更する。

# defaults/main.yml で生成先を変更する ssl_cert_tmp_dir: "{{ lookup('env', 'HOME') }}/.ansible/ssl"

4. x509_certificate_infoの日付計算でずれが出る

証明書の not_after フィールドはUTCで返ってくる。サーバーのタイムゾーンがJSTに設定されている場合、ansible_date_timeとの比較で9時間のズレが生じることがある。比較には必ずUTC基準で行う。

# ansible_date_time.iso8601_microを使いUTC比較を確実にする cert_days_remaining: >- {{ ((cert_info.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601_micro | to_datetime('%Y-%m-%dT%H:%M:%S.%fZ'))).days }}

本記事のまとめ

やりたいこと 使うモジュール・設計アプローチ
RSA秘密鍵を生成する community.crypto.openssl_privatekey
CSRを生成する community.crypto.openssl_csr
自己署名証明書を生成する community.crypto.x509_certificate(provider: selfsigned)
証明書の期限残日数を確認する community.crypto.x509_certificate_info + set_fact
30日未満で自動更新する when条件 + include_tasks
Let's Encryptの証明書を取得する community.crypto.acme_certificate(acme_version: 2)
秘密鍵をリポジトリに安全に保存する Ansible Vault(ansible-vault encrypt)
生成処理をコントローラーに集約する delegate_to: localhost
証明書管理は「一度Playbookを作れば後はAnsibleに任せられる」典型的なタスクだ。手動で更新し続けるよりも、roleとしてコード化してスケジュール実行に乗せる方が安全で確実だ。

community.cryptoの他のモジュール(PKCS#12生成・証明書失効リスト管理など)も活用すれば、証明書ライフサイクル全体をAnsibleで管理できる。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、Ansible・Vault・role設計を含む実践的なLinux構成管理をLinux Master Pro Seminarで習得できます。詳細はansible.linuxmaster.jpからご確認ください。

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録

運営

リナックスマスター.JP(代表 宮崎智広)

株式会社イーネットマーキュリーが運営するLinux教育サイトです。代表の宮崎智広は現役のLinuxサーバー管理者として20年以上の実務経験を持ち、累計3,100名以上のエンジニアを指導してきました。実践型のLinuxセミナーや無料マニュアルの配布を通じてLinux人材の育成に取り組んでいます。