记录:尝试 HashiCorp Vault + AWS IAM + AWS Lambda

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"]
}

再试一次,大功告成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章