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"]
}
再试一次,大功告成。