1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

IBM Bobで実装しながら理解する:HashiCorp Vault PKI External CA機能 - Vault 2.0新機能

1
Last updated at Posted at 2026-05-27

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-ca secrets 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公式ドキュメント:

無料で始めてみましょう

無料トライアルでは30日間40Bobコインまで利用ができます。申し込みはこちら

Bobコインの追加が必要な場合は、こちらから少額よりご購入いただけます。

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/nginxsystemctl 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サーバー側で以下を設定しました。

  1. AppRole認証メソッドの有効化
  2. pki-external-caマウントの作成
  3. ACMEアカウント設定
  4. ACMEロール設定
  5. Vault Agent用AppRoleの作成
  6. 証明書要求用ポリシーの付与
  7. Role IDとSecret IDの出力

以下は、今回のTerraform実装で実際に使っていたリソースをベースに、説明のため一部を具体値に置き換えた例です。実際の実装では変数から値を受け取っている箇所があります。

terraform/main.tf
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_endpointconfig/acme-account/<name>を作成していました。

terraform/main.tf
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 = truedisable_delete = trueになっているのは、このエンドポイントが読み取りや削除をサポートしていないためです。これはTerraform実装時の重要な注意点です。

3.3 Role設定

次に、どのドメインに対して証明書要求を許可するかをRoleで定義します。

terraform/main.tf
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関連のエンドポイントにアクセスできるポリシーが必要です。今回の実装では次のポリシーを付与していました。

terraform/policies.tf
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 target roles/<role>/acme/ paths.

一方、今回の検証では、公式記述との差分が2段階ありました。

  • まず、公式記述は roles/<role>/acme/ を示しているのに対し、実装で使っていたポリシーと実際のアクセスパスは role/<role>/acme/ でした
  • さらに、実監査ログでは role/<role>/acme/ だけでなく、role/<role>/cached への read も発生していました

つまり、確認できた相違点は次の2つです。

  1. roles/<role>/acme/ ではなく role/<role>/acme/ だったこと
  2. それに加えて role/<role>/cached への read 権限も必要だったこと

この2つ目のread権限が不足しているとVault Agentは証明書取得に失敗します。詳細は6章で説明します。

3.5 AppRole設定

Vault AgentはAppRoleで認証するため、Role IDとSecret IDを発行する設定も必要です。

terraform/main.tf
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で出しています。

terraform/outputs.tf
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チャレンジを返せる構成になっていないと証明書発行に進めません。今回の実装では次の設定でした。

terraform/nginx/user-data/nginx.sh
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設定の要点は次のとおりです。

terraform/nginx/user-data/nginx.sh
location /.well-known/acme-challenge/ {
    root /usr/share/nginx/html;
    try_files $uri =404;
}

また、HTTPS側ではVault Agentが出力した証明書と秘密鍵を参照します。

terraform/nginx/user-data/nginx.sh
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設定をまとめて行います。

/etc/vault-agent.d/vault-agent.hcl
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.shexec実行するためのtriggerとして使っている
  • 更新タイミングはpercent_renew_before_expiry = 30で制御する

Template実行の仕組み

重要な動作原理:

  1. 証明書が更新される(または初回取得)
  2. pki_external_caが証明書ファイルを書き込む
  3. Template serverが再起動される(PKI external CA certificate updated)
  4. pkiCertExternalCa関数が新しい証明書を返す
  5. .Cert | base64Encodeで新しいエンコード値を生成
  6. /tmp/vault-triggerの内容が変わる
  7. renderedイベント発生
  8. execreload-nginx.sh実行
  9. 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再起動を安全に実行するスクリプト:

scripts/reload-nginx.sh
#!/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段階です。

  1. Vault設定側でRole ID / Secret IDをoutputする
  2. Vault Agent設定側でremote stateからそれらを取得する
  3. templatefile/etc/vault-agent.d/vault-agent.hclrole-idsecret-idを生成する

これにより、Vault Agentは起動時に自律的にVaultへ認証できます。

5.2 NGINXとの依存関係

今回の実装では、NGINXがVault Agentに依存するようsystemd overrideを設定しました。

terraform/vault-agent/user-data/vault-agent.sh
[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.allowedfalse
  • つまり、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.path
  • operation
  • policy_results.allowed
  • error
  • remote_address

Audit Logでは、機密値はHMAC化されて記録されます。
そのため、トークンやシークレットの生値がそのまま出るわけではありません。

7.2 journalctl -u vault-agent

Vault Agent側では、以下の観点でログを見ると挙動を追いやすいです。

  • Starting CA manager
  • Acquiring certificate
  • Using cached certificate
  • Wrote certificate files
  • Scheduled 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パラメータを一時的に変更することで、即座に新しい証明書を取得できます。

手順:

  1. Vault Agentを停止
sudo systemctl stop vault-agent
  1. 設定ファイルの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
  1. 既存の証明書ファイルを削除(オプション)
sudo rm -f /etc/nginx/ssl/acme.*
  1. Vault Agentを起動
sudo systemctl start vault-agent
  1. ログで新しい証明書取得を確認
sudo journalctl -u vault-agent | grep -E "Acquiring|Wrote certificate"
  1. 新しい証明書の発行日時を確認
sudo openssl x509 -in /etc/nginx/ssl/acme.crt -noout -startdate -enddate -serial
  1. テスト完了後、設定を元に戻す
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点です。

  1. Failed to acquire initial certificateの原因特定にはVault Audit Logが必須だったこと

    • Vault AgentのTraceログでも詳細が出力されなかった
    • Vault Audit Logでpki-external-ca/role/web-servers/cachedへのpermission deniedを特定
  2. 公式ドキュメントの要件説明だけでは不足し、role/<role>/cachedへのread権限が必要だったこと

    • ドキュメントにはroles/<role>/acme/への権限が記載されているが、実際はrole/<role>/acme/rolesではなくrole
    • さらにrole/<role>/cachedエンドポイントへのread権限も必須
  3. 証明書更新後にスクリプトを実行するためにはTemplateブロックが必要だったこと

    • 証明書取得・更新自体はpki_external_caが行う
    • Templateブロックは更新後にexecでスクリプトを実行するために使う
    • 固定文字列では証明書更新時にexecが実行されない
    • 証明書の内容に依存する動的な値を使用する必要がある
  4. NGINX再起動用のtemplateは、実行ユーザーと出力先ファイル所有権、そしてtemplate出力内容の差分有無まで含めて設計する必要があったこと

    • /tmp/vault-triggerはVault Agent実行ユーザー(nginx)が更新できる所有権が必要
    • 証明書の内容に依存する動的な値(Base64エンコード値)を使用
    • sudoers設定でnginxユーザーにnginx -tsystemctl reload nginxを許可

9. 参考資料

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?