在現代應用程式開發中,如何安全地管理機敏資料(如資料庫密碼、API 金鑰)是一大挑戰,上篇已經介紹怎麼建立 AppRole、Policy、Secret。這篇文章將介紹如何利用 HashiCorp Vault、HashiCorp Vault Agent,打造一套自動化且安全的機敏資料存取流程。

Vault Agent 是 HashiCorp Vault 提供的一個客戶端守護程序(Client Daemon),它的主要功能和特點如下:
核心功能:
- Auto-Auth(自動認證):自動處理與 Vault 的認證過程,並管理 token 的更新
- Cache(快取):在本地快取 Vault 響應,減少對 Vault 服務器的請求
- Template(模板):將 Vault 中的密碼渲染到檔案系統中
Auto-Auth:
- 自動處理初始認證過程
- 自動更新認證 token
- 支援多種認證方法(如 AppRole、AWS、Azure、GCP 等)
- 確保應用程式始終有有效的認證憑證
Cache 功能:
- 在本地快取 Vault 響應
- 減少對 Vault 服務器的直接請求
- 提高應用程式性能
- 支援在網路中斷時使用快取的數據
Template 功能:
- 將 Vault 中的秘密渲染到檔案系統
- 支援動態更新:當秘密更改時自動更新檔案
- 支援多種模板格式
- 可以觸發應用程式重新載入
使用場景:
- 容器環境(Docker、Kubernetes)
- 雲端環境(AWS、Azure、GCP)
- 微服務架構
- 需要動態秘密管理的應用程式
開發環境
- Windows 11 Home
- Windows Terminal 1.20.11781.0
- Vault 1.17.6
- Rider 2025.1.2
- .NET 8
流程圖
這是透過 AI 根據專案產生出來的循序圖,希望可以協助大家理解整個流程

建立 Vault Server
vault server -dev
取得 Root Token

Admin
建立機敏性資料
建立的流程如下
- 啟用 KV v2 秘密引擎和 AppRole 認證方法
- 建立機敏性資料
- 建立 Client 的 AppRole 和 Policies
- 建立 Admin 的 AppRole 和 Policies
public async Task SetupVaultAsync()
{
// 啟用 KV v2 秘密引擎和 AppRole 認證方法
await EnableAppRoleAsync();
await EnableSecretAsync();
// 建立機敏性資料
await SetupSecretAsync();
// 建立 Client 的 AppRole 和 Policies
await SetupPolicesAsync(SecretPolicies);
await SetupAppRoleAsync(SecretPolicies);
// 建立 Admin 的 AppRole 和 Policies
await SetupPolicesAsync(AdminPolicies);
await SetupAppRoleAsync(AdminPolicies);
}
Policy
有以下兩個 policy
app-admin policy:用這個 policy 頒發 admin token,admin token 只能有產生 secret id/role id 這兩個行為
Note:避免將 root token 給非管理者使用。
app-dev policy:用這個 policy,讀取 dev/data/db/connection/* 以下所有的資源。
private const string SecretRootPathName = "dev";
public readonly List<Secret> Secrets =
[
new Secret("Account", "root"),
new Secret("Password", "123456"),
];
public readonly Dictionary<string, string> AdminPolicies = new()
{
["app-admin"] = """
# 允許為特定 AppRole 產生 secret id/role id
path "auth/approle/role/+/secret-id" {
capabilities = ["create", "update"]
}
path "auth/approle/role/+/role-id" {
capabilities = ["read"]
}
"""
};
public readonly Dictionary<string, string> SecretPolicies = new()
{
["app-dev"] = $$"""
path "{{SecretRootPathName}}/data/db/connection/*" {
capabilities = ["read"]
}
""",
};
建立 Policy
private async Task SetupPolicesAsync(Dictionary<string, string> policies)
{
// 建立政策
foreach (var (policyName, policyContent) in policies)
{
Console.WriteLine($"建立 {policyName} 政策...");
var writePolicyResult = await _vaultApiClient.WritePolicyAsync(policyName, policyContent);
Console.WriteLine($"政策 {policyName} 建立完成");
}
}
AppRole
啟用 AppRole
private async Task EnableAppRoleAsync()
{
try
{
Console.WriteLine("啟用 AppRole 認證方法...");
var enableAuthMethodResult = await _vaultApiClient.EnableAuthMethodAsync("approle");
Console.WriteLine("AppRole 認證方法啟用完成");
}
catch (Exception e)
{
Console.WriteLine($"AppRole 可能已經啟用: {e.Message}");
}
}
建立 AppRole
private async Task SetupAppRoleAsync(Dictionary<string, string> policies)
{
foreach (var (roleName, _) in policies)
{
Console.WriteLine($"建立 {roleName} 的 AppRole...");
var createAppRoleResult = await _vaultApiClient.CreateAppRoleAsync(
roleName: roleName,
policies: roleName,
tokenTtl: "1h",
tokenMaxTtl: "4h"
);
Console.WriteLine($"{roleName} 的 AppRole 建立完成");
}
}
主程序
- 建立 Policy、AppRole、Secret Data
- 取得 Admin Token
- 測試 Admin Token 是否有效
using System.Text.Json.Nodes;
using Lab.HashiCorpVault.Client;
namespace Lab.HashiCorpVault.Admin;
class Program
{
private static string VaultServer = "http://127.0.0.1:8200";
private static string VaultRootToken = "你的 Vault Root Token";
static async Task Main(string[] args)
{
Console.WriteLine("Setup Vault Starting...");
Console.Write("Please enter your Vault root token: ");
string vaultRootToken = Console.ReadLine() ?? VaultRootToken;
if (string.IsNullOrWhiteSpace(vaultRootToken))
{
Console.WriteLine("Error: Vault token cannot be empty.");
return;
}
var vaultApiClient = new VaultApiClient(new HttpClient
{
BaseAddress = new Uri(VaultServer),
}, vaultRootToken);
var setup = new VaultAppRoleSetup2(vaultApiClient,vaultRootToken);
// var setup = new VaultAppRoleSetup(VaultServer, vaultRootToken);
await setup.SetupVaultAsync();
// 建立管理者 Token
var adminTokens = await setup.CreateAdminToken();
// 測試使用管理者 Token 取得 app-dev 的 AppRole 認證資訊
string adminToken = adminTokens["app-admin"];
var id = await setup.GetAppRoleCredentialsAsync(adminToken, "app-dev");
Console.WriteLine($"Role ID: {id.RoleId}, Secret ID: {id.SecretId}");
// 印出管理者 Token
string tokenString = string.Join(", ", adminTokens.Select(t => $"{t.Key} token: {t.Value}"));
Console.WriteLine($"{tokenString}");
Console.WriteLine("Setup Vault Completed.");
}
}
啟動專案
輸入 Root Token,取得 Admin Token

Agent
新增一個 Console 專案,加入 vault-agent-config.hcl、dev-db-config.ctmpl
vault-agent-config.hcl
配置 auto_auth、cache、template,以及 vault agent 的 位置為 0.0.0.0:8100
# Vault Agent 配置
exit_after_auth = false
pid_file = "./vault-agent.pid"
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "role_id"
secret_id_file_path = "secret_id"
}
}
sink "file" {
config = {
path = "vault-token"
}
}
}
# 快取配置
cache {
use_auto_auth_token = true
}
# 範本配置 - 用於獲取實際的秘密
template {
source = "dev-db-config.ctmpl"
destination = "dev-db-config.json"
}
vault {
address = "http://127.0.0.1:8200"
}
listener "tcp" {
address = "0.0.0.0:8100"
tls_disable = true
}
dev-db-config.ctmpl
{{ with secret "dev/data/db/connection/identity" }}
{
"account": "{{ .Data.data.Account }}",
"password": "{{ .Data.data.Password }}"
}
{{ end }}
設定 Vault Agent 主流程
- 通過 Admin Token 取得 Role ID 和 Secret ID 認證信息
- 將認證信息寫入文件
- 啟用 Vault Agent
public async Task SetupVaultAgentAsync()
{
// 取得 Role ID 和 Secret ID(這步驟通常由管理員執行)
var (roleId, secretId) = await GetAppRoleCredentials(AppRoleName);
// 將認證信息寫入文件(實際環境中應該通過安全的方式傳遞)
await SaveCredentialsForVaultAgent(AppRoleName, roleId, secretId);
// 啟動 Vault Agent
await StartVaultAgent();
}
ROLE_ID_FILE、SECRET_ID_FILE 這兩個檔案需要對應到 vault-agent-config.hcl
private async Task SaveCredentialsForVaultAgent(string roleName, string roleId, string secretId)
{
Console.WriteLine("儲存認證資訊供 Vault Agent 使用...");
// 建立目錄
var credentialsDir = Path.Combine("vault-credentials", roleName);
if (!Directory.Exists(credentialsDir))
{
Directory.CreateDirectory(credentialsDir);
}
credentialsDir = "";
// 儲存 Role ID 和 Secret ID
await File.WriteAllTextAsync(Path.Combine(credentialsDir, ROLE_ID_FILE), roleId);
await File.WriteAllTextAsync(Path.Combine(credentialsDir, SECRET_ID_FILE), secretId);
Console.WriteLine($"認證資訊已儲存到 {credentialsDir} 目錄");
}
對應位置如下圖:

透過 Vault CLI 啟動 Vault Agent
private async Task StartVaultAgent()
{
Console.WriteLine("啟動 Vault Agent...");
var startInfo = new ProcessStartInfo
{
FileName = "vault",
Arguments = $"agent -config={AGENT_CONFIG_FILE} -log-level=trace", // 使用 trace 級別
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = false,
WorkingDirectory = Directory.GetCurrentDirectory()
};
using var process = new Process { StartInfo = startInfo };
process.OutputDataReceived += (sender, e) =>
{
if (string.IsNullOrEmpty(e.Data) == false)
{
Console.WriteLine($"[AGENT] {e.Data}");
// 特別關注這些關鍵訊息
if (e.Data.Contains("template") || e.Data.Contains("rendered") ||
e.Data.Contains("error") || e.Data.Contains("failed"))
{
Console.WriteLine($"*** 重要: {e.Data} ***");
}
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (string.IsNullOrEmpty(e.Data) == false)
{
Console.WriteLine($"[ERROR] {e.Data}");
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// 等待更長時間觀察日誌
Console.WriteLine("觀察 Vault Agent 日誌 5 秒...");
await Task.Delay(TimeSpan.FromSeconds(5));
await VerifyVaultAgentFileAsync();
await VerifyVaultProcessInfoAsync();
}
主程序步驟如下
- 建立 Vault Agent
- 從 vault-token 取得 token
- 取得機敏性資料
class Program
{
private static string VaultServer = "http://127.0.0.1:8200/";
private static string VaultToken = "你的 token";
private static string VaultAgentAddress = "http://127.0.0.1:8100/";
static async Task Main(string[] args)
{
Console.WriteLine("Vault Agent Starting...");
Console.Write("Please enter your Vault token: ");
string vaultToken = Console.ReadLine() ?? VaultToken;
if (string.IsNullOrWhiteSpace(vaultToken))
{
Console.WriteLine("Error: Vault token cannot be empty.");
return;
}
// 設定 Vault Agent
var setup = new VaultAgentSetup2(new VaultApiClient(new HttpClient
{
BaseAddress = new Uri(VaultServer),
}, vaultToken));
await setup.SetupVaultAgentAsync();
Console.WriteLine("Vault Agent Started.");
Console.WriteLine("透過 Vault Agent 讀取秘密...");
var agentToken = await File.ReadAllTextAsync("vault-token");
var vaultApiClient = new VaultApiClient(new HttpClient
{
BaseAddress = new Uri(VaultAgentAddress),
}, agentToken);
vaultApiClient.UpdateToken(agentToken);
var secretResult = await vaultApiClient.GetSecretAsync("dev/data/db/connection/identity");
var secretData = secretResult["data"]?["data"] as JsonObject;
if (secretData == null)
{
Console.WriteLine("Error: Unable to read secret data.");
return;
}
if (secretData.ContainsKey("Account") && secretData.ContainsKey("Password"))
{
Console.WriteLine($"Account: {secretData["Account"]}");
Console.WriteLine($"Password: {secretData["Password"]}");
Console.WriteLine("讀取秘密成功");
}
else
{
Console.WriteLine("Error: Account or Password not found in secret data.");
}
}
}
啟動專案
輸入 Admin Token

除了讀取機敏性資料,我也觀察了一下相關的檔案、進程是不是已經被建立起來,也可以取得 Agent Token

Client
#Lab.HashiCorpVault.Client
操作 Vault 的行為,查找官方文件 + AI 就能實現出來,就不再多贅述
範例所有的操作都使用 Vault API/CLI
API
- AppRole:https://developer.hashicorp.com/vault/api-docs/auth/approle
- Token:https://developer.hashicorp.com/vault/api-docs/auth/token
- KV:https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Lab.HashiCorpVault.Client;
public class VaultApiClient(HttpClient client, string vaultToken)
{
private string _vaultToken = vaultToken;
public void UpdateToken(string token)
{
_vaultToken = token;
}
public async Task<JsonObject> EnableAuthMethodAsync(string authMethod, string path = null)
{
var payload = new
{
type = authMethod
};
// 如果 path 為 null 或空字串,則使用 authMethod 作為路徑
string authPath = string.IsNullOrEmpty(path) ? authMethod : path;
return await SendRequestAsync(HttpMethod.Post, $"/v1/sys/auth/{authPath}", payload);
}
public async Task<JsonObject> EnableSecretEngineAsync(string engine, string path)
{
var payload = new
{
type = engine
};
return await SendRequestAsync(HttpMethod.Post, $"/v1/sys/mounts/{path}", payload);
}
public async Task<JsonObject> WriteSecretAsync(string path, Dictionary<string, string> data)
{
var payload = new
{
data
};
return await SendRequestAsync(HttpMethod.Post, $"/v1/{path}", payload);
}
public async Task<JsonObject> WritePolicyAsync(string name, string policy)
{
var payload = new
{
policy
};
return await SendRequestAsync(HttpMethod.Put, $"/v1/sys/policies/acl/{name}", payload);
}
public async Task<JsonObject> CreateAppRoleAsync(string roleName, string policies, string tokenTtl, string tokenMaxTtl)
{
var payload = new
{
token_policies = policies,
token_ttl = tokenTtl,
token_max_ttl = tokenMaxTtl
};
return await SendRequestAsync(HttpMethod.Post, $"/v1/auth/approle/role/{roleName}", payload);
}
public async Task<JsonObject> GetRoleIdAsync(string roleName)
{
return await SendRequestAsync(HttpMethod.Get, $"/v1/auth/approle/role/{roleName}/role-id");
}
public async Task<JsonObject> GetSecretAsync(string path)
{
return await SendRequestAsync(HttpMethod.Get, $"/v1/{path}");
}
public async Task<JsonObject> GenerateSecretIdAsync(string roleName)
{
return await SendRequestAsync(HttpMethod.Post, $"/v1/auth/approle/role/{roleName}/secret-id");
}
public async Task<JsonObject> CreateTokenAsync(string policy, string ttl)
{
var payload = new
{
policies = new[] { policy },
ttl
};
return await SendRequestAsync(HttpMethod.Post, "/v1/auth/token/create", payload);
}
private async Task<JsonObject> SendRequestAsync(HttpMethod method, string url, object payload = null)
{
var request = new HttpRequestMessage(method, url);
request.Headers.Add("X-Vault-Token", _vaultToken);
if (payload != null)
{
var json = JsonSerializer.Serialize(payload);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await client.SendAsync(request);
return await HandleResponseAsync(response);
}
private async Task<JsonObject> HandleResponseAsync(HttpResponseMessage response)
{
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Vault API request failed with status {response.StatusCode}: {content}");
}
if (string.IsNullOrWhiteSpace(content))
{
return new JsonObject();
}
return JsonNode.Parse(content)?.AsObject() ?? new JsonObject();
}
}
範例位置
心得
這個範例斷斷續續的花了我一個月的時間,稍微了解了 Vault Agent 的生命週期,但是實務上還是有一些我覺得困惑的點,例如,Admin Token 的狀態傳遞?是要在服務一起動的時候就注入?
Agent 一啟動之後,就會開始在背景執行展期的工作,我嘗試著將 AppRole 的時間由原本的 1h 變成 1m

觀察一下 Agent Token 的狀態
$Env:VAULT_ADDR = "http://127.0.0.1:8200"
vault token lookup hvs.CAESIMKcxFpOsKRtOzPLMiQNulcgkBrrZaV8Abyejg7a-c-OGh4KHGh2cy5IM0RkNk1pNUxBQ01kcFVKcE01d2haNFc
可以觀察到,Token 的生命週期不斷的被展期

另外,Agent 一旦被關閉,整個流程就要重新來過
範例位置
這個範例使用 Vault CLI/Vault API,文章主要以 API 為主,檔名以 2 結尾的代表用 API。
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET