Vault 是 HashiCorp 開源的密鑰庫程序。
從 https://www.vaultproject.io/downloads 下載得到的 CLI 程序包括既包括客戶端也包括服務端實現。
本文以 Windows + PowerShell + Nodejs 爲環境描述用法,其它環境基本相同。
Vault 基本用法
運行開發服務器:
vault server -dev
開發服務器會直接使用 HTTP 而不啓用 HTTPS,在 http://127.0.0.1:8200 。
開發服務器會默認啓用 Vault UI,在 http://127.0.0.1:8200/ui 。
根據運行後的提示,客戶端的用法是:
$env:VAULT_ADDR="http://127.0.0.1:8200"
vault status
vault auth list
靜態的密鑰讀寫:
vault kv put secret/hello foo=1 bar=a
vault kv get secret/hello
這裏的 secret
是一個默認的存儲庫(Secret Engine),hello
是具體的密鑰,而 foo bar
是密鑰中的多組鍵值對。
vault secrets list
vault kv list secret/
密鑰可以刪除,刪除後能讀出來被刪除的元信息。
vault kv delete secret/hello
vault kv get secret/hello
雖然 CLI 提供了 vault kv
命令,但其實 API 只有 read
接口,CLI 也提供它。這都有助於本地實驗。
vault read secret/data/hello
Vault SDK 用法
Vault 官方不提供 Node.js SDK。有一個相當完整的第三方庫 node-vault
。
基本用法:
const NodeVault = require('node-vault');
const vault = NodeVault({
apiVersion: 'v1',
endpoint: 'http://127.0.0.1:8200',
token: 'xxx',
});
if (require.main === module) {
(async () => {
const res = await vault.read('secret/data/hello');
console.log(res);
})();
}
場景與集成過程
如果在 AWS Lambda 中想使用某個密鑰,第一個想法是從環境變量傳進來。但這樣會產生一個風險,因爲可能很多 AWS 子賬號都能看到這個信息。
同理,按上述的 SDK 基本用法傳 VAULT_TOKEN
,問題是一樣的。
Vault 利用了 AWS STS 提供的 GetCallerIdentity
接口,設計了一個身份驗證方式,並在驗證成功後生成一個臨時 token 用於訪問。這稱爲 IAM 鑑權方式,是爲了區別於另一種基於 EC2 的鑑權方式。
首先,在 AWS IAM 中創建一個新的 lambda role,並附加以下策略(Policies):
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"iam:GetUser",
"iam:GetRole",
"sts:GetCallerIdentity"
],
"Effect": "Allow",
"Resource": "*"
}
]
}
你會得到一個 role arn。
vault auth enable aws
vault write auth/aws/role/dev-iam `
auth_type=iam `
bound_iam_principal_arn=$ARN_ROLE `
token_policies=default `
token_ttl=600s
這裏的 dev-iam
是 Vault Role,default
是 Vault Policy,token_ttl
是臨時 token 的有效期。
接下來是限定 AWS 憑據範圍,減輕重放攻擊的一個配置:
vault write auth/aws/config/client iam_server_id_header_value=dev
然後,我們需要生成一個有效的 AWS STS GetCallerIdentity 請求,但不由 AWS SDK 發起,而是傳給 Vault 使用,藉此換取臨時的 Vault token。
事實上 AWS SDK 不直接提供這個能力,雖然一切細節都是公開的。
const sts = new AWS.STS();
function getAwsRequest(iamServerId) {
const request = sts.getCallerIdentity();
request.httpRequest.headers['X-Vault-AWS-IAM-Server-ID'] = iamServerId; // configured in vault
request.emit('build', [request]);
request.emit('afterBuild', [request]);
request.emit('sign', [request]);
// console.log(request.httpRequest);
return request.httpRequest;
}
function toGolangStyleHeaders(headers) {
const ret = {};
for (const [k,v] of Object.entries(headers)) {
ret[k] = [v];
}
return ret;
}
function toVaultRequest(request) {
// console.log(request);
const encoded_url = Buffer.from(request.endpoint.href, 'utf-8').toString('base64');
const encoded_body = Buffer.from(request.body, 'utf-8').toString('base64');
const golang_headers = toGolangStyleHeaders(request.headers);
const encoded_headers = Buffer.from(JSON.stringify(golang_headers), 'utf-8').toString('base64');
return {
'role': 'dev-iam', // Vault Role
'iam_http_request_method': request.method,
'iam_request_url': encoded_url,
'iam_request_body': encoded_body,
'iam_request_headers': encoded_headers,
}
}
async function awsLoginVault() {
const iamServerId = 'dev';
const awsRequest = getAwsRequest(iamServerId);
const vaultRequest = toVaultRequest(awsRequest);
const response = await vault.awsIamLogin(vaultRequest);
// console.log(response);
vault.token = response.auth.client_token;
// const status = await vault.status();
// console.log(status);
}
事實上這裏得到的 vaultRequest
也能使用 CLI 試用:
vault write auth/aws/login \
role=dev-role-iam \
iam_http_request_method=POST \
iam_request_url=$encoded_url \
iam_request_body=$encoded_body \
iam_request_headers=$encoded_headers
測試之:
async function getVaultSecret() {
const response = await vault.read('secret/data/hello');
// console.log(response);
return response.data.data;
}
失敗了,很正常,還差一步。
上文選擇了使用 default
policy,但默認地,在這個 policy 中並未配置對 secret/*
的訪問權。
vault policy list
vault policy read default > default-policy.hcl
// 根據下文修改文件
vault policy write default default-policy.hcl
在 default-policy.hcl
中追加一段:
path "secret/*" {
capabilities = ["read"]
}
再試一次,大功告成。