【実務・中級編】PuppetとHashiCorp Vaultの統合!ダイナミックシークレットを安全にマニフェストへ流し込む高度な認証・連携術 – インフラ構成管理(IaC)活用バイブル

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の自動化スピードを犠牲にすることなく、セキュリティレベルをトップクラスに引き上げるこのアーキテクチャを、ぜひあなたのプロダクション環境にも導入してほしい。コードの美しさとセキュリティの堅牢性が高次元で調和した瞬間、インフラエンジニアとしての醍醐味を改めて実感できるはずだ。

タイトルとURLをコピーしました