保護你的機敏性資料,通過 C# VaultSharp 訪問 Hashicorp Vault - 快速入門

開發者不應該把機敏性資料,用明碼的方式存放在電腦,HashiCorp Vault 是一套基於身份的私鑰和加密管理系統,Hashicorp Vault 是一個安全管理工具,專門用於管理機敏性資料,例如:密碼、API 密鑰和其他機密訊息,下圖出自 Introduction | Vault | HashiCorp Developer

原本我本來想要自幹一套 Secret Manager / Config Manage,因為有 Vault 我省去了自幹的時間,這裡,我將嘗試學習使用 C# VaultSharp 訪問 Hashicorp Vault

 

開發環境

  • Windows 11 Home
  • Powershell
  • vault 1.17.6
  • Rider 2024.2

安裝

下列方法擇一

scoop install vault
choco install vault

本機環境啟動開發用的 Vault Server

vault server -dev

 

Vault Server 已經被建立起來,Console  介面可以看到 Unseal Key、Root Token,這兩個是很重要的東西,不應該洩漏或是用明碼的方式儲存在電腦裡面

...
2024-10-05T21:45:22.824+0800 [INFO]  core: successful mount: namespace="" path=secret/ type=kv version="v0.19.0+builtin"
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.
You may need to set the following environment variables:
PowerShell:
   $env:VAULT_ADDR="http://127.0.0.1:8200"
cmd.exe:
   set VAULT_ADDR=http://127.0.0.1:8200
The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.
Unseal Key: wjSRAfJc5udiO8NGStdvDTcN1hpxNFqQUWylq9otabc=
Root Token: hvs.WqTOKcy2PUR6TNGsJoDssxN4
Development mode should NOT be used in production installations!

由於使用 -dev,他會把 root token 放在本機電腦,可以用指令查看內容

Get-Content $env:USERPROFILE\.vault-token

 

設定服務位置的環境變數

預設服務位置為 'http://127.0.0.1:8200' ,因為還沒有憑證,所以先額外設定 http,環境變數為 VAULT_ADDR

$Env:VAULT_ADDR = "http://127.0.0.1:8200"

 

在瀏覽器訪問 http://127.0.0.1:8200

  • Method=Token
  • Token=hvs.WqTOKcy2PUR6TNGsJoDssxN4

Vault CLI

或是尻尻看 health check

PS C:\Users\yao> curl http://127.0.0.1:8200/v1/sys/health | jq
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100   391  100   391    0     0   6470      0 --:--:-- --:--:-- --:--:--  6516
{
 "initialized": true,
 "sealed": false,
 "standby": false,
 "performance_standby": false,
 "replication_performance_mode": "disabled",
 "replication_dr_mode": "disabled",
 "server_time_utc": 1728137740,
 "version": "1.17.6",
 "enterprise": false,
 "cluster_name": "vault-cluster-015c7103",
 "cluster_id": "040bcba4-dba1-772b-c208-924850df5f56",
 "echo_duration_ms": 0,
 "clock_skew_ms": 0,
 "replication_primary_canary_age_ms": 0
}

 

啟用一個 kv-v2 secret agent

PS C:\Users\yao> vault secrets enable --version=2 --path=job/dream-team kv
Success! Enabled the kv secrets engine at: job/dream-team/

 

停用一個 secret agent

PS C:\Users\yao> vault secrets disable job/dream-team
Success! Disabled the secrets engine (if it existed) at: job/dream-team/

 

確認有哪一些 secrets

PS C:\Users\yao> vault secrets list --detailed
Path               Plugin       Accessor              Default TTL    Max TTL    Force No Cache    Replication    Seal Wrap    External Entropy Access    Options           Description                                                UUID                                    Version    Running Version          Running SHA256    Deprecation Status
----               ------       --------              -----------    -------    --------------    -----------    ---------    -----------------------    -------           -----------                                                ----                                    -------    ---------------          --------------    ------------------
cubbyhole/         cubbyhole    cubbyhole_750a37f1    n/a            n/a        false             local          false        false                      map[]             per-token private secret storage                           f61f51eb-8e7d-b4a1-0b6d-05f69cafeb9a    n/a        v1.17.6+builtin.vault    n/a               n/a
identity/          identity     identity_c91022cc     system         system     false             replicated     false        false                      map[]             identity store                                             b4e37eb6-e0fd-f404-e43a-c773e3d395c0    n/a        v1.17.6+builtin.vault    n/a               n/a
job/dream-team/    kv           kv_af041c1c           system         system     false             replicated     false        false                      map[version:2]    n/a                                                        9fc90ba1-7ade-487f-5e7a-cf84e0c34c8c    n/a        v0.19.0+builtin          n/a               supported
kv/                kv           kv_b99cedb5           system         system     false             replicated     false        false                      map[]             n/a                                                        be1a0a6f-db6d-57d8-b4e2-60397c384240    n/a        v0.19.0+builtin          n/a               supported
secret/            kv           kv_a5a2f1da           system         system     false             replicated     false        false                      map[version:2]    key/value secret storage                                   4f147440-052d-a6ad-fde0-b082d8dfe3f2    n/a        v0.19.0+builtin          n/a               supported
sys/               system       system_0a34ed05       n/a            n/a        false             replicated     true         false                      map[]             system endpoints used for control, policy and debugging    dd8d92a1-439d-1e06-19db-09b0dbf2b803    n/a        v1.17.6+builtin.vault    n/a               n/a

 

寫入資料到 kv-v2

  • -mount=job/dream-team:-mount 是一個標誌,指定 KV 引擎的掛載路徑。job/dream-team 是實際的掛載路徑,表示這個 KV 引擎位於 "job/dream-team" 路徑下。
  • my-secret:這是要創建或更新的密鑰的名稱。
  • UserName=admin Password=123456:存放的 Key/Value。在這個例子中,有兩個 Key/Value:
    UserName = admin
    Password = 123456
PS C:\Users\yao> vault kv put -mount=job/dream-team my-secret UserName=admin Password=123456
======== Secret Path ========
job/dream-team/data/my-secret
======= Metadata =======
Key                Value
---                -----
created_time       2024-10-05T16:59:02.6649909Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

 

從 kv-v2 讀取資料

PS C:\Users\yao> vault kv get -mount=job/dream-team my-secret
======== Secret Path ========
job/dream-team/data/my-secret
======= Metadata =======
Key                Value
---                -----
created_time       2024-10-05T17:11:05.9499243Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1
====== Data ======
Key         Value
---         -----
Password    123456
UserName    admin

 

C# VaultSharp

適用於 HashiCorp Vault 的最全面的跨平台 .NET  套件。

dotnet add package VaultSharp --version 1.17.5.1

接下來使用  C#  的操作 Vault,必須要傳入 Authentication,這一篇我們用的是 Token,它在一開始建立的 Vault Server 的 Console 對話視窗中

建立 Vault Client 的範例如下

// 初始化 Vault Client
var authMethod = new TokenAuthMethodInfo(this.VaultToken);
var vaultClientSettings = new VaultClientSettings(vaultServer, authMethod);
var vaultClient = new VaultClient(vaultClientSettings);

 

啟用一個 kv-v2 secret agent

vaultClient.V1.System.MountSecretBackendAsync():建立 Secret Agent,範例如下:

[TestMethod]
public async Task 建立KV_V2()
{
    // 設定 Vault Server 和 Token
    var vaultServer = "http://127.0.0.1:8200";

    // 初始化 Vault Client
    var authMethod = new TokenAuthMethodInfo(this.VaultToken);
    var vaultClientSettings = new VaultClientSettings(vaultServer, authMethod);
    var vaultClient = new VaultClient(vaultClientSettings);

    // 啟用 KV V2 存儲引擎
    var mountPath = "job/dream-team"; // 要啟用的路徑
    await CreateKvV2Path(vaultClient, mountPath);
    Console.WriteLine($"KV V2 secrets engine enabled at path: {mountPath}");
}

 

private static async Task CreateKvV2Path(IVaultClient vaultClient, string path)
{
    try
    {
        await vaultClient.V1.System.MountSecretBackendAsync(new SecretsEngine
        {
            Path = path,
            Type = new SecretsEngineType("kv-v2")
        });

        Console.WriteLine($"Successfully created kv-v2 path: {path}");
    }
    catch (VaultApiException ex)
    {
        if (ex.Message.Contains("path is already in use"))
        {
            Console.WriteLine($"The path '{path}' is already in use. It may already be mounted.");
        }
        else
        {
            Console.WriteLine($"Error creating kv-v2 path: {ex.Message}");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Unexpected error: {ex.Message}");
    }
}

 

停用一個 secret agent

vaultClient.V1.System.UnmountSecretBackendAsync():停用 Secrect Agent,範例如下:

[TestMethod]
public async Task 停用KV_V2()
{
    // 設定 Vault Server 和 Token
    var vaultServer = "http://127.0.0.1:8200";

    // 初始化 Vault Client
    var authMethod = new TokenAuthMethodInfo(this.VaultToken);
    var vaultClientSettings = new VaultClientSettings(vaultServer, authMethod);
    var vaultClient = new VaultClient(vaultClientSettings);

    var secretPath = "my-secret";
    var mountPath = "job/dream-team";
    await vaultClient.V1.System.UnmountSecretBackendAsync(mountPath);

    try
    {
        var secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(secretPath, mountPoint: mountPath);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

 

kv-v2 讀寫資料

  • vaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync():寫入 Key/Value
  • vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync():讀取 Key/Value
[TestMethod]
public async Task 讀寫KV_V2()
{
    // 設定 Vault Server 和 Token
    var vaultServer = "http://127.0.0.1:8200";

    // 初始化 Vault Client
    var authMethod = new TokenAuthMethodInfo(this.VaultToken);
    var vaultClientSettings = new VaultClientSettings(vaultServer, authMethod);
    var vaultClient = new VaultClient(vaultClientSettings);

    // 寫入 Secrets (等同於 vault kv put)
    var secretData = new Dictionary<string, object>
    {
        { "username", "admin" },
        { "password", "123456" }
    };

    var secretPath = "my-secret";
    var mountPath = "job/dream-team";

    await vaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(secretPath, secretData, mountPoint: mountPath);
    Console.WriteLine("Secrets 已成功寫入!");

    Console.WriteLine("讀取KV-V2:");
    var secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(secretPath, mountPoint: mountPath);
    Console.WriteLine($"username={secret.Data.Data["username"]}");
    Console.WriteLine($"password={secret.Data.Data["password"]}");
}

 

心得

完成了一個很簡單的 CRUD 操作,在實作的過程中,想要嘗試著使用 SecureString 加強安全性,避免應用程式在做 Memmory Dump 的時候洩漏了敏感性資料,但我弄不出來,在操作的過程中一定會使用到 string,忍痛宣告放棄,有興趣的可以參考一下這裡

範例位置

sample.dotblog/Secrets Manager/Lab.HashiCorpVault/Lab.HashiCorpVault.Test at c222f656e0a23ccd184c989d46b7d72e935e61b2 · yaochangyu/sample.dotblog (github.com)

參考

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo