記錄:嘗試 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"]
}

再試一次,大功告成。

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