PuppetとHashiCorp Vaultの完全統合:ダイナミックシークレットを安全にマニフェストへ流し込む実践アーキテクチャ
テックリードの私たちが日々直面する最大のジレンマの一つは、「インフラの自動化(IaC)とセキュリティの担保」のトレードオフだ。
Puppetによる完全な構成管理の実現には機密情報(DBパスワード、APIトークン、TLS証明書)が不可欠だが、それをHiera(YAML)に直書きしたり、Gitリポジトリに含めたりすることは、セキュリティインデクスにおいて「自殺行為」に等しい。暗号化ツール(Eyaml等)を用いたとしても、鍵のライフサイクル管理やローテーションの複雑化という別の負債を生む。
この悪夢を断ち切る唯一の解が、HashiCorp VaultとPuppetの動的統合だ。
今回は、ハードコードされたシークレットを完全に廃し、Puppetエージェントの実行ごとにVaultからダイナミックシークレット(一時的な認証情報)を安全に取得・適用する、プロダクションレベルのアーキテクチャを徹底解説する。
—
1. 全体アーキテクチャとセキュリティ境界の設計
まずは、Puppet ServerとVaultがどのようにセキュアに通信し、シークレットを解決するのか、そのライフサイクルデザインを定義する。
[Puppet Agent] —> (1. 構成適用要求 / 固有のAppRole ID提示) —> [Puppet Server]
|
(2. Vaultへトークン要求)
v
[Target Node] <--- (4. 一時トークンで動的シークレット取得) <--- [HashiCorp Vault]
|
(5. マニフェストへ流し込み・適用)
1. AppRole認証の活用: Puppet Server(あるいはエージェントノード)は、長期的なRootトークンではなく、厳格にスコープされた`AppRole`を使用してVaultに認証する。
2. Just-In-Time(JIT)発行: シークレットはPuppetのコンパイル時、またはエージェントの適用時に動的に取得され、ディスク上に永続化されない。
3. 自動破棄: 取得されたトークンやシークレットは極めて短いTTL(Time To Live)を持ち、処理完了後速やかに破棄される。
—
2. Vault側のセットアップ:AppRoleとポリシーの構築
まずはVault側で、Puppetからの安全なアクセスを受け入れるための基盤を整える。
2.1 権限ポリシーの定義 (`puppet-policy.hcl`)
Puppetサーバーがアクセスできるパスを、必要最小限の権限(Principle of Least Privilege)に絞る。
/etc/vault/policies/puppet-policy.hcl
データベースのダイナミックシークレット生成パスへのアクセス許可
path “database/creds/app-db-role” {
capabilities = [“read”]
}
KV Secrets Engine (Version 2) へのアクセス許可
path “secret/data/production/app/” {
capabilities = [“read”]
}
このポリシーをVaultに登録する。
vault policy write puppet-policy /etc/vault/policies/puppet-policy.hcl
2.2 AppRole認証バックエンドの有効化
AppRole有効化
vault auth enable approle
Puppet用ロールの作成(トークンTTLを短く設定するのがプロの技)
vault write auth/approle/role/puppet-server \
secret_id_ttl=10m \
token_ttl=1h \
token_max_ttl=4h \
policies=”puppet-policy”
—
3. Puppet環境への統合:`puppet-vault`モジュールの導入と設定
PuppetからVaultを叩くためのデファクトスタンダードである、`puppetlabs-vault`(またはコミュニティの成熟したモジュール)を導入する。
3.1 依存関係の定義 (`Puppetfile`)
チーム開発におけるバージョン固定は絶対のルールだ。`Puppetfile`に以下を記述する。
Puppetfile
mod ‘puppetlabs-stdlib’, ‘9.3.0’
mod ‘puppetlabs-vault’, ‘1.4.0’
3.2 Puppet Server側の接続設定 (`vault.yaml`)
Puppet ServerがVaultと通信するための認証情報を定義する。このファイルはセキュアな権限(`0400`, owner: `pe-puppet`等)で保護すること。
/etc/puppetlabs/puppet/vault.yaml
—
Vaultサーバーのエンドポイント
address: “https://vault.internal.net:8200”
AppRole認証の設定
auth_method: “approle”
mount_point: “approle”
role_id: “your-app-role-id-here”
secret_id: “your-app-secret-id-here”
SSL/TLS検証(プロダクションでは必ず内部CA証明書を指定)
ssl_cacert: “/etc/puppetlabs/puppet/ssl/certs/vault_ca.pem”
—
4. マニフェスト実装:ダイナミックシークレットの安全な流し込み
ここからが本番だ。データベースのダイナミックシークレット(接続の度に自動生成され、有効期限が切れるユーザー・パスワード)をVaultから引き出し、設定ファイルにレンダリングするPuppetコードを記述する。
4.1 ファンクションを通じたシークレットの取得と適用
PuppetのHieraバックエンド、またはカスタムファンクション経由でVaultを叩く。
site/profile/manifests/database.pp
class profile::database {
# Vaultからダイナミックシークレットを動的に取得
# 返り値は Hash (username と password が含まれる)
$db_secret = vault_lookup(‘database/creds/app-db-role’, ‘vault.yaml’)
# 取得したダイナミックシークレットを変数に格納
$db_user = $db_secret[‘username’]
$db_password = $db_secret[‘password’]
# アプリケーションの設定ファイルに安全に流し込む
file { ‘/etc/app/database.config’:
ensure => file,
owner => ‘app’,
group => ‘app’,
mode => ‘0400’, # 最小限の権限
content => epp(‘profile/database.config.epp’, {
‘db_user’ => $db_user,
‘db_password’ => $db_password,
‘db_host’ => ‘db.internal.net’,
}),
notify => Service[‘app-service’], # 変更検知でサービス再起動
}
service { ‘app-service’:
ensure => running,
enable => true,
}
}
4.2 EPPテンプレートのベストプラクティス
ERBではなく、より安全でスコープが明確なEPPテンプレートを使用する。
<%# site/profile/templates/database.config.epp %>
==========================================
Managed by Puppet. DO NOT EDIT MANUALLY.
Dynamic Secrets provided by HashiCorp Vault
==========================================
[database]
host = <%= $db_host %>
username = <%= $db_user %>
password = <%= $db_password %>
sslmode = require
—
5. チーム開発を加速するプロのプラクティスと設定
日々の開発スピードを落とせず、かつセキュアな状態を維持するための「現場の知見」を共有する。
5.1 開発環境(Local)でのモック戦略
ローカルのPuppet開発(Rake / PDK / Bolt)でVaultサーバーを常に立てておくのは重荷だ。テスト時にはVaultのレスポンスをモック化する仕組みを取り入れる。
spec/classes/database_spec.rb
require ‘spec_helper’
describe ‘profile::database’ do
let(:node) { ‘test.internal.net’ }
# vault_lookup ファンクションのスタブ化
before(:each) do
Puppet::Functions.create_function(:vault_lookup) do
def vault_lookup(path, _config)
if path == ‘database/creds/app-db-role’
{ ‘username’ => ‘mock_user’, ‘password’ => ‘mock_password’ }
end
end
end
end
it ‘renders database config with vault secrets’ do
is_expected.to contain_file(‘/etc/app/database.config’)
.with_content(/username = mock_user/)
.with_content(/password = mock_password/)
end
end
5.2 チーム開発におけるコーディング規約(Linter設定)
`puppet-lint` にカスタムチェックを加え、マニフェスト内にハードコードされたパスワードや機密文字列が含まれていないかをCI/CDパイプライン(GitHub Actions / GitLab CI)で強制的に弾く。
Rakefileでのpuppet-lint設定
Rake::Task[:lint].clear
PuppetLint::RakeTask.new(:lint) do |t|
t.disable_checks = [‘class_inherits_from_params_class’]
t.warning_level = :all
t.strict = true
# 自製プラグインや正規表現でパスワード様の文字列を検出するカスタムチェックを入れるのが極意
end
—
6. まとめ:運用の神髄は「持たざるインフラストラクチャ」にある
今回紹介したVaultとPuppetの統合アプローチの真の価値は、「ノードがシークレットの永続的な所有権を持たないこと」にある。
万が一、ターゲットノードが侵害されたとしても、そこに残るのはTTLが切れた無効な一時トークンか、すでにローテーションされた古い認証情報だけであり、システム全体への影響を最小限に抑え込むことができる。
IaCの自動化スピードを犠牲にすることなく、セキュリティレベルをトップクラスに引き上げるこのアーキテクチャを、ぜひあなたのプロダクション環境にも導入してほしい。コードの美しさとセキュリティの堅牢性が高次元で調和した瞬間、インフラエンジニアとしての醍醐味を改めて実感できるはずだ。