IBM Bobで実装しながら理解する:HashiCorp Vault PKI External CA機能 - Vault 2.0新機能をTerraformで構築
1. はじめに
本記事は、HashiCorp Vault 2.0で追加されたPKI External CA機能をこれから実装する人向けに、IBM Bobを使って検証した内容を整理したものです。
Vault PKI External CA機能は、Vault 2.0で追加された新機能で、Vault AgentがACMEクライアントとして動作し、パブリック認証局(Let's Encryptなど)から証明書を自動取得・更新するための機能です。
今回の検証で使用した技術:
- Vault Enterpriseの
pki-external-casecrets engine - Vault Agentの
pki_external_ca設定 - NGINXのHTTP-01チャレンジ応答(2026-05-27時点ではHTTP-01のみサポート)
- Terraformによるインフラ構成管理
この記事で扱う内容:
- PKI External CA機能を実装するときに最低限理解しておくべき構成
- Terraformでの設定ポイント
- トラブルシューティング時のログの見方(Vault Audit Log、journalctl)
- 公式ドキュメントでは解決できなかった点
この記事で扱わない内容:
- EC2インスタンスのセットアップ
- Vaultのインストール
- NGINXのインストール
HashiCorp公式ドキュメント:
2. 検証環境の概要
2.1 コンポーネント
今回の検証で扱った主要コンポーネントは次の4つです。
| コンポーネント | バージョン | 役割 | 配置・実行形態 | 今回の記事で扱う範囲 |
|---|---|---|---|---|
| Vault Enterprise | v2.0.0+ent |
pki-external-ca secrets engineを提供 |
別EC2インスタンスで稼働(本検証では同一VPC内) | 設定と監査ログ |
| Vault Agent | v2.0.0+ent | ACMEクライアントとして証明書取得・更新 | NGINXと同一EC2インスタンスで稼働(必須)、nginxユーザーで実行 |
設定とjournalctl
|
| NGINX | 1.24.0 | HTTP-01チャレンジ応答とHTTPS終端 | Vault Agentと同一EC2インスタンスで稼働(必須)、master processはroot、worker processはnginxユーザー |
前提設定 |
| Let's Encrypt Staging | - | ACMEサーバー(検証用の外部Public CA) | 外部サービス | 外部連携先 |
2.2 アーキテクチャ図
以下の図は、Vault PKI External CA機能の全体アーキテクチャを示しています。
前提条件: Vault AgentとNGINXは同一ホストで稼働
Vault Agentがチャレンジファイルと証明書をローカルへ書き込み、NGINXが同じローカルファイルシステムからそれらを参照するため、両者は同一ホスト上に配置する必要があります。
実行ユーザーとファイル権限:
| 対象 | 所有者・実行ユーザー | 権限 | 用途・補足 |
|---|---|---|---|
| Vault Agent | nginx |
— | セキュリティのため最小権限で実行 |
/etc/vault-agent.d/ |
nginx:nginx |
0700 |
Vault Agent設定、role-id、secret-id、token格納先 |
/usr/share/nginx/html/.well-known/acme-challenge/ |
nginx:nginx |
0755 |
HTTP-01チャレンジファイル格納先 |
/etc/nginx/ssl/ |
root:root |
0755 |
証明書・秘密鍵格納ディレクトリ |
| 証明書ファイル | root:nginx |
0640 |
Vault Agentがsudoで書き込み、NGINXが参照 |
/tmp/vault-trigger |
nginx:nginx |
0600 |
NGINX再起動用triggerファイル |
/usr/local/bin/reload-nginx.sh |
root:root |
0755 |
NGINX再起動スクリプト |
| NGINX再起動 | nginx |
sudoers許可 |
/usr/sbin/nginx、systemctl reload/restart nginx を実行可能にする |
本検証では、VaultとNGINXホストを別々のEC2インスタンスとして構成していますが、Vault AgentとNGINXは必ず同一ホスト上に配置する必要があります。
2.3 処理シーケンス
以下のシーケンス図は、Vault AgentがパブリックCA(Public CA)から証明書を取得する際の処理フローを示しています。
HTTP-01チャレンジの仕組み
HTTP-01チャレンジでは、Public CAが対象ドメインの /.well-known/acme-challenge/ 配下へHTTPでアクセスし、配置されたチャレンジトークンを取得できることを確認してドメインを検証します。
本環境での動作:
- Vault AgentがVaultを通じてPublic CAへ証明書要求
- Public CAがチャレンジトークンを発行
- Vault Agentがトークンファイルを
/usr/share/nginx/html/.well-known/acme-challenge/へ書込 - NGINXがポート80でチャレンジパスを公開
- Public CAがHTTPでチャレンジトークンを取得して検証
- 検証成功後、Public CAが証明書を発行
重要なポイント:
- Vault Agentの
challenge_path(/usr/share/nginx/html/.well-known/acme-challenge)とNGINXのrootディレクティブ(/usr/share/nginx/html)+location(/.well-known/acme-challenge/)の実体パスが一致している必要がある - ポート80がインターネットから到達可能である必要がある
- チャレンジファイルは一時的なもので、検証後は不要
本検証では、Let's Encrypt Staging環境を使用して動作確認を行いました。
シーケンス図の要点は、Vault Agentが証明書取得だけでなく、取得後にtemplateのexecでNGINX再起動まで自動化している点です。
3. 設定手順
ここでは、今回の検証で実際に設定した内容を、実装者向けに分解して説明します。設定はVaultサーバー側 → NGINXホスト側 → Vault Agent側の順で行いました。
3.1 Vaultサーバー側の設定
まず、Vaultサーバー側で以下を設定しました。
- AppRole認証メソッドの有効化
-
pki-external-caマウントの作成 - ACMEアカウント設定
- ACMEロール設定
- Vault Agent用AppRoleの作成
- 証明書要求用ポリシーの付与
- Role IDとSecret IDの出力
以下は、今回のTerraform実装で実際に使っていたリソースをベースに、説明のため一部を具体値に置き換えた例です。実際の実装では変数から値を受け取っている箇所があります。
resource "vault_auth_backend" "approle" {
type = "approle"
}
resource "vault_mount" "pki_external_ca" {
path = "pki-external-ca"
type = "pki-external-ca"
}
この2つのリソースがないと、Vault AgentはAppRole認証もPKI External CA要求も実行できません。
3.2 ACMEアカウント設定
パブリックCA連携では、まずACMEアカウントをVaultへ登録します。今回の実装ではvault_generic_endpointでconfig/acme-account/<name>を作成していました。
resource "vault_generic_endpoint" "acme_account" {
path = "${vault_mount.pki_external_ca.path}/config/acme-account/letsencrypt-staging"
data_json = jsonencode({
directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
email_contacts = ["mailto:admin@vault-xxx.com"]
key_type = "ec256"
})
disable_read = true
disable_delete = true
}
設定値の意味は次のとおりです。
| 項目 | 意味 |
|---|---|
directory_url |
接続先ACMEディレクトリURL(例: Let's Encrypt Staging) |
email_contacts |
ACME通知用の連絡先(例: mailto:admin@vault-xxx.com) |
key_type |
ACMEアカウント鍵の種類(例: ec256) |
ここでdisable_read = true、disable_delete = trueになっているのは、このエンドポイントが読み取りや削除をサポートしていないためです。これはTerraform実装時の重要な注意点です。
3.3 Role設定
次に、どのドメインに対して証明書要求を許可するかをRoleで定義します。
resource "vault_generic_endpoint" "acme_role" {
path = "${vault_mount.pki_external_ca.path}/role/web-servers"
data_json = jsonencode({
acme_account_name = "letsencrypt-staging"
allowed_domains = ["nginx-direct.vault-xxx.com"]
allowed_domain_options = ["exact"]
allowed_challenge_types = ["http-01"]
})
depends_on = [vault_generic_endpoint.acme_account]
disable_read = true
disable_delete = true
}
Roleは「証明書発行の認可ルール」と理解すると分かりやすいです。特に重要なのは次の項目です。
-
acme_account_name- どのACMEアカウントを使うか
-
allowed_domains- どのドメインを要求可能にするか
-
allowed_domain_options- bare domains / subdomains / wildcards / globs のどれで判定するか
-
allowed_challenge_types- 今回は
http-01
- 今回は
3.4 Policy設定
Vault AgentがPKI External CA機能を使うには、Roleやorder関連のエンドポイントにアクセスできるポリシーが必要です。今回の実装では次のポリシーを付与していました。
path "${vault_mount.pki_external_ca.path}/config/acme" {
capabilities = ["read"]
}
path "${vault_mount.pki_external_ca.path}/acme/*" {
capabilities = ["read", "create", "update"]
}
path "${vault_mount.pki_external_ca.path}/role/+/acme/*" {
capabilities = ["read", "create", "update"]
}
path "${vault_mount.pki_external_ca.path}/roles/*" {
capabilities = ["read"]
}
path "${vault_mount.pki_external_ca.path}/role/+/new-order" {
capabilities = ["create", "update"]
}
path "${vault_mount.pki_external_ca.path}/role/+/order/*" {
capabilities = ["read", "create", "update"]
}
path "${vault_mount.pki_external_ca.path}/role/+/order/*/certificate" {
capabilities = ["read"]
}
path "${vault_mount.pki_external_ca.path}/role/+/active-orders" {
capabilities = ["list"]
}
path "${vault_mount.pki_external_ca.path}/role/+/cached" {
capabilities = ["read"]
}
この中で今回の検証で最も重要だったのが、最後のrole/<role>/cachedです。
公式ドキュメントでは次のように説明されています。
The agent's auth method must grant update access to the PKI mount's
acme/*paths and the targetroles/<role>/acme/paths.
一方、今回の検証では、公式記述との差分が2段階ありました。
- まず、公式記述は
roles/<role>/acme/を示しているのに対し、実装で使っていたポリシーと実際のアクセスパスはrole/<role>/acme/でした - さらに、実監査ログでは
role/<role>/acme/だけでなく、role/<role>/cachedへのreadも発生していました
つまり、確認できた相違点は次の2つです。
-
roles/<role>/acme/ではなくrole/<role>/acme/だったこと - それに加えて
role/<role>/cachedへのread権限も必要だったこと
この2つ目のread権限が不足しているとVault Agentは証明書取得に失敗します。詳細は6章で説明します。
3.5 AppRole設定
Vault AgentはAppRoleで認証するため、Role IDとSecret IDを発行する設定も必要です。
resource "vault_approle_auth_backend_role" "acme_role" {
backend = vault_auth_backend.approle.path
role_name = "web-servers"
token_policies = [vault_policy.pki_external_ca_requester.name]
token_ttl = 3600
token_max_ttl = 14400
secret_id_ttl = 0
secret_id_num_uses = 0
}
実装上のポイントは次のとおりです。
-
role_nameはACMEロール名にそろえている -
token_policiesに証明書要求ポリシーを付ける -
secret_id_ttl = 0- Secret IDを期限切れにしない
-
secret_id_num_uses = 0- Secret IDを回数無制限にする
さらに、後でVault Agentへ受け渡せるように、Role IDとSecret IDをTerraform outputで出しています。
output "nginx_role_id" {
value = vault_approle_auth_backend_role.acme_role.role_id
sensitive = true
}
output "nginx_secret_id" {
value = vault_approle_auth_backend_role_secret_id.acme_secret_id.secret_id
sensitive = true
}
3.6 NGINXホスト側の設定
NGINX側は、HTTP-01チャレンジを返せる構成になっていないと証明書発行に進めません。今回の実装では次の設定でした。
mkdir -p /usr/share/nginx/html/.well-known/acme-challenge
mkdir -p /etc/nginx/ssl
chown -R nginx:nginx /usr/share/nginx/html/.well-known
NGINX設定の要点は次のとおりです。
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
try_files $uri =404;
}
また、HTTPS側ではVault Agentが出力した証明書と秘密鍵を参照します。
ssl_certificate /etc/nginx/ssl/acme.crt;
ssl_certificate_key /etc/nginx/ssl/acme.key;
初回起動ではプレースホルダー証明書も配置していました。これは、Vault Agentの初回取得前でもNGINXを起動可能にするためです。
3.7 Vault Agent側の設定
最後に、Vault Agent側の設定を行いました。
Vault Agent側では、AppRole認証、PKI External CA設定、証明書出力先、そして証明書更新後にNGINX再起動スクリプトを実行するためのtemplate trigger設定をまとめて行います。
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/etc/vault-agent.d/role-id"
secret_id_file_path = "/etc/vault-agent.d/secret-id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/etc/vault-agent.d/token"
}
}
}
vault {
address = "https://<vault fqdn>"
}
pki_external_ca "web-tls" {
mount_path = "pki-external-ca"
role = "web-servers"
challenge_type = "http-01"
identifiers {
dns = ["nginx-direct.vault-xxx.com"]
}
http_01 {
challenge_path = "/usr/share/nginx/html/.well-known/acme-challenge"
}
destination {
path = "/etc/nginx/ssl"
pem_bundle = false
filename_prefix = "acme"
umask = "077"
}
percent_renew_before_expiry = 30
}
# templateブロックでNGINX再起動を実行
# pkiCertExternalCa関数で証明書のPEM内容を取得し、contentsを動的にする
# 証明書が更新されると内容が変わるため、execが実行される
template {
destination = "/tmp/vault-trigger"
perms = "0600"
contents = <<-EOT
{{- with pkiCertExternalCa "web-tls" -}}
{{- .Cert | base64Encode -}}
{{- end }}
EOT
exec {
command = ["/usr/local/bin/reload-nginx.sh"]
timeout = "60s"
}
}
設定の要点は次のとおりです。
- Vault AgentはAppRoleでVaultへログインする
- 証明書要求先のRoleは
web-servers - HTTP-01チャレンジファイルは
/usr/share/nginx/html/.well-known/acme-challengeへ置く - 取得した証明書と秘密鍵は
/etc/nginx/ssl/acme.crt、/etc/nginx/ssl/acme.keyとして利用される - 証明書の取得・更新自体は
pki_external_caブロックが行う - templateブロックは証明書発行そのものには不要で、更新後に
reload-nginx.shをexec実行するためのtriggerとして使っている - 更新タイミングは
percent_renew_before_expiry = 30で制御する
Template実行の仕組み
重要な動作原理:
- 証明書が更新される(または初回取得)
-
pki_external_caが証明書ファイルを書き込む - Template serverが再起動される(PKI external CA certificate updated)
-
pkiCertExternalCa関数が新しい証明書を返す -
.Cert | base64Encodeで新しいエンコード値を生成 -
/tmp/vault-triggerの内容が変わる -
renderedイベント発生 -
execでreload-nginx.sh実行 - Nginxが新しい証明書をリロード
ここで重要なのは、証明書の取得・更新そのものはpki_external_caが完結しており、templateはその後段でスクリプト実行を発火させるためにだけ存在することです。
つまり、証明書ファイルを更新するだけならtemplateは不要で、今回templateを置いている理由は、証明書更新を契機にNGINX再起動を自動実行したいためです。
固定文字列の問題:
- 固定文字列(例:
"Certificate updated")では、証明書更新時に/tmp/vault-triggerの内容が変わらない - 内容が変わらない →
renderedイベントが発生しない →execが実行されない - そのため、証明書の内容に依存する動的な値(Base64エンコード値)を使用する必要がある
ファイル所有権の重要性:
-
/tmp/vault-triggerはVault Agent実行ユーザー(nginx)が更新できる所有権が必要 -
root:rootの場合、operation not permittedエラーでtemplate更新自体が失敗する - 初期化:
install -o nginx -g nginx -m 0600 /dev/null /tmp/vault-trigger
reload-nginx.shスクリプト
NGINX再起動を安全に実行するスクリプト:
#!/bin/bash
set -euo pipefail
TAG="vault-agent-exec"
log_info() { logger -t "$TAG" -p user.info "$*"; }
log_error() { logger -t "$TAG" -p user.err "$*"; }
log_info "Starting nginx reload triggered by certificate update..."
# Test nginx configuration
if ! sudo nginx -t 2>&1 | logger -t "$TAG" -p user.info; then
log_error "nginx configuration test failed"
exit 1
fi
log_info "nginx configuration test passed"
# Reload nginx
if ! sudo systemctl reload nginx 2>&1 | logger -t "$TAG" -p user.info; then
log_error "nginx reload failed, attempting restart..."
if ! sudo systemctl restart nginx 2>&1 | logger -t "$TAG" -p user.info; then
log_error "nginx restart also failed"
exit 1
fi
log_info "nginx restarted successfully"
else
log_info "nginx reloaded successfully"
fi
log_info "Certificate update and nginx reload completed"
sudoers設定:
# /etc/sudoers.d/vault-agent
nginx ALL=(ALL) NOPASSWD: /usr/sbin/nginx
nginx ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx
nginx ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
動作確認ログ
2026-05-27T07:40:22.101Z [INFO] agent.template.server: template server received new token
2026-05-27T07:40:22.118Z [INFO] agent: (runner) creating new runner (dry: false, once: false)
2026-05-27T07:40:22.145Z [INFO] agent.pkiexternalca.server.ca[0]: Wrote certificate files: cert=/etc/nginx/ssl/acme.crt key=/etc/nginx/ssl/acme.key
2026-05-27T07:40:22.149Z [INFO] agent: (runner) rendered "/tmp/vault-trigger"
May 27 07:40:22 vault-agent-exec[12114]: nginx reloaded successfully
May 27 07:40:22 vault-agent-exec[12115]: Certificate update and nginx reload completed
2026-05-27T07:40:22.201Z [INFO] agent: (runner) received finish
このように、証明書ファイル書込 → template render → exec 実行 → 完了、という流れで動作します。
4. 手動ワークフローによる動作確認
Vault Agent自動化に入る前に、Vault CLIを使った手動ワークフローでパブリックCA連携の動作を確認しました。
4.1 新規オーダーの作成
まず、対象ドメインで新しいオーダーを作成します。
vault write pki-external-ca/role/<role-name>/new-order \
identifiers="<masked-domain>"
実行結果:
{
"order_id": "abc123...",
"status": "pending",
"identifiers": ["example.com"]
}
4.2 オーダーステータスの確認
オーダー作成後、状態を確認します。
vault read pki-external-ca/role/<role-name>/order/<order-id> -format=json
実行結果:
{
"status": "awaiting_challenge_fulfillment",
"identifiers": ["example.com"],
"authorizations": [...]
}
ステータスがawaiting_challenge_fulfillmentになっていることを確認します。
4.3 チャレンジ情報の取得
次に、HTTP-01チャレンジのトークンとkey_authorizationを取得します。
vault read pki-external-ca/role/<role-name>/order/<order-id> \
identifier="<masked-domain>" \
challenge_type="http-01" \
-format=json
実行結果:
{
"token": "abc123token",
"key_authorization": "abc123token.xyz789fingerprint"
}
4.4 チャレンジファイルの配置と確認
取得したkey_authorizationを、NGINXが配信できる場所へ配置します。
# 環境変数に設定
export TOKEN="abc123token"
export KEY_AUTH="abc123token.xyz789fingerprint"
# チャレンジファイルを配置
echo -n "${KEY_AUTH}" | sudo tee /usr/share/nginx/html/.well-known/acme-challenge/${TOKEN}
# 外部からアクセス可能か確認
curl http://<masked-domain>/.well-known/acme-challenge/${TOKEN}
実行結果:
abc123token.xyz789fingerprint
チャレンジファイルが正しく配置され、外部からアクセス可能であることを確認します。
4.5 チャレンジ完了の通知
チャレンジファイルが外部から取得できることを確認した後、Vaultへ完了通知を送ります。
vault write pki-external-ca/role/<role-name>/order/<order-id> \
identifier="<masked-domain>" \
challenge_type="http-01"
実行結果:
{
"status": "processing"
}
Vaultへチャレンジ完了を通知すると、Public CAが検証を開始します。
4.6 証明書の取得
状態がcompletedになったら、証明書を取得できます。
vault read pki-external-ca/role/<role-name>/order/<order-id>/certificate -format=json
実行結果:
{
"certificate": "-----BEGIN CERTIFICATE-----\n...",
"issuing_ca": "-----BEGIN CERTIFICATE-----\n...",
"ca_chain": ["-----BEGIN CERTIFICATE-----\n..."],
"private_key": "-----BEGIN PRIVATE KEY-----\n...",
"serial_number": "03:ab:cd:..."
}
証明書が正常に発行されました。この手動ワークフローの成功により、Vault PKI External CA機能とHTTP-01チャレンジの外部疎通が正常に動作していることを確認できました。
5. Vault Agentによる自動取得の設定と挙動
5.1 TerraformからVault Agentへ値が渡る流れ
今回の実装では、Vault設定側で出力したRole IDとSecret IDを、Vault Agent設定側でremote state経由で取得し、Vault Agent設定へ埋め込みました。
実装の流れは次の3段階です。
- Vault設定側でRole ID / Secret IDをoutputする
- Vault Agent設定側でremote stateからそれらを取得する
-
templatefileで/etc/vault-agent.d/vault-agent.hcl、role-id、secret-idを生成する
これにより、Vault Agentは起動時に自律的にVaultへ認証できます。
5.2 NGINXとの依存関係
今回の実装では、NGINXがVault Agentに依存するようsystemd overrideを設定しました。
[Unit]
Requires=vault-agent.service
After=vault-agent.service
この設定により、Vault Agentが先に動き、その後にNGINXが再起動される構成になっています。証明書ファイルの配置順序を安定させるために重要です。
5.3 成功時のjournalctlログ
Vault Agentが正常に証明書を取得できた場合、以下のようなログが出力されます。
2026-05-22T12:47:52.145Z [INFO] agent.pkiexternalca.server.ca[0]: Starting CA manager
2026-05-22T12:47:52.151Z [INFO] agent.pkiexternalca.server.ca[0]: Acquiring certificate
2026-05-22T12:47:52.163Z [INFO] agent.pkiexternalca.server.ca[0]: Using cached certificate: serial=<masked>
2026-05-22T12:47:52.165Z [DEBUG] agent.pkiexternalca.server.ca[0]: Wrote file atomically: path=/etc/nginx/ssl/acme.key
2026-05-22T12:47:52.167Z [DEBUG] agent.pkiexternalca.server.ca[0]: Wrote file atomically: path=/etc/nginx/ssl/acme.crt
2026-05-22T12:47:52.167Z [INFO] agent.pkiexternalca.server.ca[0]: Wrote certificate files: cert=/etc/nginx/ssl/acme.crt key=/etc/nginx/ssl/acme.key
2026-05-22T12:47:52.167Z [INFO] agent.pkiexternalca.server.ca[0]: Scheduled certificate renewal: renewal_time="<masked>" duration=<masked>
このログから、次の事実が読み取れます。
- Vault Agentは起動時に証明書取得処理へ入った
- 既存のキャッシュ済み証明書を利用した
- 証明書と秘密鍵を
/etc/nginx/ssl配下へアトミックに書き込んだ - 次回更新時刻をスケジュールした
6. 失敗したときに何を見ればよいか
6.1 最初の症状
失敗時の最初の症状は次の1行でした。
[ERROR] agent.pkiexternalca.server.ca[0]: Failed to acquire initial certificate
このエラーメッセージだけでは、Role設定ミス、HTTP-01疎通の問題、Policy不足のいずれが原因かを判別できません。
6.2 Vault Agentログだけでは原因が足りなかった
今回の検証では、Vault Agentのログレベルを上げても根本原因を特定できませんでした。そのため、Vault Audit Logを有効化して、Vault側で実際にどのパスが拒否されたかを確認する必要がありました。
6.3 Audit Logから何が読めたか
Audit Log有効化後、失敗時には次の内容が記録されました。
{
"auth": {
"policies": ["default", "pki-external-ca-requester"],
"policy_results": {"allowed": false}
},
"error": "1 error occurred:\n\t* permission denied\n\n",
"request": {
"operation": "read",
"path": "pki-external-ca/role/web-servers/cached",
"remote_address": "<masked>"
}
}
ここから読み取れた事実は次のとおりです。
- 拒否されたのは
pki-external-ca/role/web-servers/cached - 操作は
read -
policy_results.allowedはfalse - つまり、HTTP-01以前にVault側の権限で失敗していた
6.4 修正後に成功したことをどう確認したか
role/<role>/cachedのread権限を追加後、Vault Agentログで次の成功ログを確認できました。
2026-05-22T12:47:52.151Z [INFO] agent.pkiexternalca.server.ca[0]: Acquiring certificate
2026-05-22T12:47:52.163Z [INFO] agent.pkiexternalca.server.ca[0]: Using cached certificate: serial=<masked>
2026-05-22T12:47:52.167Z [INFO] agent.pkiexternalca.server.ca[0]: Wrote certificate files: cert=/etc/nginx/ssl/acme.crt key=/etc/nginx/ssl/acme.key
さらに、Vault Audit Log側でも、同じcachedパスへのreadが成功していました。
{
"time": "2026-05-22T12:47:52.160107435Z",
"type": "request",
"auth": {
"policies": ["default", "pki-external-ca-requester"],
"policy_results": {
"allowed": true
}
},
"request": {
"operation": "read",
"path": "pki-external-ca/role/web-servers/cached",
"remote_address": "<masked>"
}
}
このため、今回の検証では、/cached権限追加が原因修正だったと判断できます。
また、公式ドキュメントとの比較で分かったことが2つあります。
エージェントの認証方法は、PKIマウントの
acme/*パスとターゲットroles/<role>/acme/パスへのupdateアクセスを許可する必要があります
- 実装で使っていたポリシーと実際のアクセスパスは、
roles/<role>/acme/ではなくrole/<role>/acme/だった - さらに、実際には
role/<role>/cachedへのread権限も必要だった
加えて、HTTP-01では Vault Agent の challenge_path と NGINX が配信する実体パスを一致させる必要があります。今回の構成では、/usr/share/nginx/html/.well-known/acme-challenge を Vault Agent が書き込み先として使い、NGINX 側でも同じ実体パスを公開していました。
7. ログ調査でよく使うコマンド
7.1 Vault Audit Log
Vault Audit Logでは、少なくとも以下を追うと原因に近づきやすいです。
request.pathoperationpolicy_results.allowederrorremote_address
Audit Logでは、機密値はHMAC化されて記録されます。
そのため、トークンやシークレットの生値がそのまま出るわけではありません。
7.2 journalctl -u vault-agent
Vault Agent側では、以下の観点でログを見ると挙動を追いやすいです。
Starting CA managerAcquiring certificateUsing cached certificateWrote certificate filesScheduled certificate renewal
7.3 journalctl -u vault
Vaultサーバー側の補助確認にも使えますが、今回の検証では主因の特定はAudit Log側が中心でした。
7.4 NGINXアクセスログ
HTTP-01チャレンジ失敗時には、NGINXアクセスログも有効です。Public CAからチャレンジトークン取得が来ているかを見れば、HTTP公開と疎通の問題を切り分けやすくなります。
7.5 証明書の強制再取得テスト方法
証明書の自動更新機能をテストする際、実際の有効期限まで待つ必要はありません。percent_renew_before_expiryパラメータを一時的に変更することで、即座に新しい証明書を取得できます。
手順:
- Vault Agentを停止
sudo systemctl stop vault-agent
- 設定ファイルの
percent_renew_before_expiryを99に変更
sudo sed -i 's/percent_renew_before_expiry = 30/percent_renew_before_expiry = 99/' /etc/vault-agent.d/vault-agent.hcl
- 既存の証明書ファイルを削除(オプション)
sudo rm -f /etc/nginx/ssl/acme.*
- Vault Agentを起動
sudo systemctl start vault-agent
- ログで新しい証明書取得を確認
sudo journalctl -u vault-agent | grep -E "Acquiring|Wrote certificate"
- 新しい証明書の発行日時を確認
sudo openssl x509 -in /etc/nginx/ssl/acme.crt -noout -startdate -enddate -serial
- テスト完了後、設定を元に戻す
sudo sed -i 's/percent_renew_before_expiry = 99/percent_renew_before_expiry = 30/' /etc/vault-agent.d/vault-agent.hcl
sudo systemctl restart vault-agent
注意事項:
-
percent_renew_before_expiryは1-99の範囲で指定する必要があります(100は無効) - 99に設定すると、証明書の有効期限の99%経過時点(ほぼ即座)に更新を試みます
- テスト後は必ず元の値(通常30)に戻してください
- Let's Encryptにはレート制限があるため、本番環境では頻繁なテストを避けてください
-
/tmp/vault-triggerを手動編集する場合は、root:rootに戻さないよう注意してください。Vault Agent はnginxユーザーで動くため、所有者がroot:rootだと template の atomic rename がoperation not permittedで失敗します
8. まとめ
今回の検証では、手動操作による確認とVault Agentによる自動化の両方で、PKI External CA機能の動作を確認できました。
特に重要だったのは次の4点です。
-
Failed to acquire initial certificateの原因特定にはVault Audit Logが必須だったこと- Vault AgentのTraceログでも詳細が出力されなかった
- Vault Audit Logで
pki-external-ca/role/web-servers/cachedへのpermission deniedを特定
-
公式ドキュメントの要件説明だけでは不足し、
role/<role>/cachedへのread権限が必要だったこと- ドキュメントには
roles/<role>/acme/への権限が記載されているが、実際はrole/<role>/acme/(rolesではなくrole) - さらに
role/<role>/cachedエンドポイントへのread権限も必須
- ドキュメントには
-
証明書更新後にスクリプトを実行するためにはTemplateブロックが必要だったこと
- 証明書取得・更新自体は
pki_external_caが行う - Templateブロックは更新後に
execでスクリプトを実行するために使う - 固定文字列では証明書更新時に
execが実行されない - 証明書の内容に依存する動的な値を使用する必要がある
- 証明書取得・更新自体は
-
NGINX再起動用のtemplateは、実行ユーザーと出力先ファイル所有権、そしてtemplate出力内容の差分有無まで含めて設計する必要があったこと
-
/tmp/vault-triggerはVault Agent実行ユーザー(nginx)が更新できる所有権が必要 - 証明書の内容に依存する動的な値(Base64エンコード値)を使用
- sudoers設定でnginxユーザーに
nginx -tとsystemctl reload nginxを許可
-