如何使用Vault管理Go应用程序的机密性

559 阅读8分钟

由于现代软件是如此复杂,它需要使用秘密和机密信息,如API密钥、令牌,以及用于连接远程服务器和数据库的老式用户名和密码。

虽然曾经人们认为将这些信息与代码本身存放在一起是可以的,但如今--特别是鉴于12因素应用程序运动--已经不再是这样了。将任何类型的安全信息保存在你的代码中被认为是不好的安全做法--有充分的理由。

因此,人们开发了一系列的方法和工具,以使凭证不在代码库中,保持它们的安全,并在需要时随时提供给代码。

在本教程中,你将学习如何用HashiCorp Vault管理Go应用程序的机密。

如果你是一个PHP开发人员,请查看本教程的PHP版本

什么是Vault?

如果这是你第一次听说Vault,根据 Vault网站,它是:

...一个基于身份的秘密和加密管理系统。它允许你使用用户界面、CLI或HTTP API来保护秘密和其他敏感数据,确保、存储和严格控制对令牌、密码、证书、加密密钥的访问。

它可以存储秘密,自动进行凭证轮换,滚动加密密钥,并提供API驱动的加密功能。通过一个统一的接口,秘密被持久化到一个底层秘密引擎。

这些引擎可以与现有的基础设施集成,如微软Azure谷歌云PostgreSQL等数据库,或RabbitMQ等队列服务器。

在本教程中,代码将使用最简单的引擎,即 KV Secrets Engine(kv)

这是一个通用的Key-Value存储,用于在Vault的配置的物理存储中存储任意的秘密。

这个引擎的另一个伟大功能是,当启用时(如本教程),秘密可以被版本化,允许它们在需要时被回滚。

启动Vault

在编写任何代码之前,为了让你快速启动和运行,通过运行以下命令,在"开发 "服务器模式下启动Vault:

vault server -dev

开发服务器模式不需要复杂的设置,可以启用密钥/值存储引擎,并预先生成一个认证令牌,这将为您节省大量的时间和精力。

千万不要在生产中使用这种模式,因为它不安全。

该命令将把类似于以下的输出写到终端:

You may need to set the following environment variable:

    $ export 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: OqoUeFDlv9PjqvJgBolLyevsj4y3gqPInNKvBubZTd0=
Root Token: hvs.2fWa1QeRWesGfjGeb2QqBYU4

Development mode should NOT be used in production installations!

从写到终端的输出中,复制写在最后的Root Token 值(例如:hvs.2fWa1QeRWesGfjGeb2QqBYU4 ),并将其粘贴到下面第二条命令中的占位符<VAULT_TOKEN> 。然后,在一个新的终端窗口中运行这些命令:

export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN=<VAULT_TOKEN>

设置项目

接下来,创建一个项目目录,改变到它,并通过运行下面的命令启用依赖性跟踪

cd
mkdir vault-go
cd vault-go
go mod init example/vault-go

然后,通过运行下面的命令,安装代码的唯一依赖(这可以节省与Vault互动的大量时间和精力),即HashiCorp的官方Go库

go get github.com/hashicorp/vault/api

创建核心应用逻辑

现在,是时候创建应用程序的核心代码了,本教程的每一节都将借鉴它。要做到这一点,在项目目录下创建一个新文件,命名为main.go。然后,将下面的代码粘贴到其中:

package main

import (
    "context"
    "log"
    "os"

    vault "github.com/hashicorp/vault/api"
)

const password string = "<PASSWORD>"

func main() {
    config := vault.DefaultConfig()
    config.Address = os.Getenv("VAULT_ADDR")

    client, err := vault.NewClient(config)
    if err != nil {
        log.Fatalf("Unable to initialize a Vault client: %v", err)
    }

    client.SetToken(os.Getenv("VAULT_TOKEN"))

    secretData := map[string]interface{}{
        "password": password,
    }

    ctx := context.Background()
}

这段代码首先导入了所有需要的包,然后声明了一个常量,password ,作为代码将存储在键/值引擎中的密码。用你选择的字符串替换占位符<PASSWORD>

然后,main() 函数开始设置Go Vault客户端将用于连接到Vault API的地址。你在前面将其存储在VAULT_ADDR 环境变量中。之后,它初始化一个新的Vault客户端,并打印出一个错误,如果在这样做的时候返回一个错误。

如果没有返回错误,则设置认证令牌,以便在向Vault API发出请求时传递;从VAULT_TOKEN 环境变量中检索。之后,它将初始化存储在Vault服务器中的秘密,并创建一个Go上下文以与Vault服务器进行交互。

存储一个秘密

在这一点上,代码的核心部分已经准备好了,所以现在是时候在密钥值存储引擎中添加存储秘密的功能了。要做到这一点,在main() 函数的底部添加以下代码:

_, err = client.KVv2("secret").Put(ctx, "my-secret-password", secretData)
if err != nil {
    log.Fatalf("Unable to write secret: %v to the vault", err)
}
log.Println("Super secret password written successfully to the vault.")

该代码试图使用密钥my-secret-password ,将秘密存储在secretData 。如果秘密不能被存储,返回的错误将被打印到终端。否则,将打印出一条确认信息,表明秘密被成功存储。

执行代码,运行下面的命令,看看秘密密码是否被成功存储:

go run main.go

现在,再次运行该代码。它应该像第一次那样成功。如果一个秘密已经存在,它的原始值不会被覆盖,相反,会创建一个新版本的秘密。

检索一个秘密

现在,一个秘密可以被存储(和版本)了,接下来要做的事情就是检索它了。将下面的代码粘贴在main() 函数的底部,前一部分的代码之后

secret, err := client.KVv2("secret").Get(ctx, "my-secret-password")
if err != nil {
    log.Fatalf(
        "Unable to read the super secret password from the vault: %v", 
        err,
    )
}

value, ok := secret.Data["password"].(string)
if !ok {
    log.Fatalf(
        "value type assertion failed: %T %#v", 
        secret.Data["password"], 
        secret.Data["password"],
    )
}

log.Printf("Super secret password [%s] was retrieved.\n", value)

该代码试图通过使用密钥 "my-secret-password "调用Get() 函数来检索秘密。如果找不到秘密或无法检索到秘密,它将打印出一条错误信息。

如果数据检索到,代码会检查它是否包含存储的密码。如果不包含,则向终端打印一条错误信息。如果它确实包含了密码,那么密码的值将被转换为一个字符串并打印到终端。

如果你运行该代码,你应该看到Super secret password <YOUR PASSWORD> was retrieved. ,其中的占位符包含你先前设置的密码。

检索一个秘密的所有版本

假设你已经创建了一个秘密的几个版本,并想查看它们的全部。要做到这一点,请添加下面的代码,以取代之前的代码,因为之前的代码只检索了一个秘密的单一版本:

versions, err := client.KVv2("secret").GetVersionsAsList(ctx, "my-secret-password")
if err != nil {
    log.Fatalf(
        "Unable to retrieve all versions of the super secret password from the vault. Reason: %v", 
        err,
    )
}

for _, version := range versions {
    deleted := "Not deleted"
    if !version.DeletionTime.IsZero() {
        deleted = version.DeletionTime.Format(time.UnixDate)
    }

    secret, err := client.KVv2("secret").
        GetVersion(ctx, "my-secret-password", version.Version)
    if err != nil {
        log.Fatalf(
            "Unable to retrieve version %d of the super secret password from the vault. Reason: %v",
            err,
        )
    }
    value, ok := secret.Data["password"].(string)

    if ok {
        log.Printf(
            "Version: %d. Created at: %s. Deleted at: %s. Destroyed: %t. Value: '%s'.\n",
            version.Version,
            version.CreatedTime.Format(time.UnixDate),
            deleted,
            version.Destroyed,
            value,
       )
    }
}

该代码调用GetVersionsAsList() 函数来检索KVVersionMetadata对象的一个片断,该对象包含秘密的每个版本的元数据。

如果返回了一个错误,它将被打印到终端。如果没有返回错误,它会遍历返回的KVVersionMetadata 对象。对于每一个对象,在打印出版本号、创建时间、删除时间(如果它已被删除)、是否已被销毁以及它的值之前,它通过调用GetVersion() 来检索版本的值。

如果你运行它,你应该看到与下面类似的输出:

Version: 1. Created at: Fri Jul 29 17:02:35 UTC 2022. Deleted at: Not deleted. Destroyed: false. Value: 'Hashi12345'.

删除一个秘密的单一版本

假设你存储了一个版本的秘密并想删除它。要做到这一点,在main() 函数的末尾粘贴以下代码:

_, err = client.KVv2("secret").Delete(ctx, "my-secret-password")
if err != nil {
    log.Fatalf("Unable to delete the latest version of the secret from the vault. Reason: %v", err)
}
log.Println("Delete the latest version of the secret from the vault")

该代码调用了Delete() 函数,该函数删除了一个秘密的最新版本,如果它可用的话。

删除一个秘密的所有版本

要添加的最后一项功能是删除一个秘密的所有版本的能力。要做到这一点,请在main() 函数的末尾添加以下代码,以取代前一节中添加的代码:

err = client.KVv2("secret").DeleteMetadata(ctx, "my-secret-password")
if err != nil {
    log.Fatalf("Unable to entirely delete the super secret password from the vault. Reason: %v", err)
}
log.Println("Deleted the latest version of the super secret password from the vault")

该代码调用DeleteMetadata() 函数,该函数删除了一个秘密的所有版本和元数据,如果该秘密存在于键值引擎中。如果返回了一个错误,它就被打印出来。否则,它将打印出一条确认信息,显示该秘密已返回到终端。

关于安全的最后一句话

有两件事值得注意:

  • 首先:本教程中的代码示例有意使用HTTP,以避免设置自签SSL证书和配置Web服务器来使用该证书。
  • 第二:虽然应用程序使用令牌与Vault互动,但对API本身的请求并不安全。因此,任何能够访问API的人都可以访问你的秘密。

在生产应用程序中,只通过HTTPS进行请求,并使用适当的认证授权,以确保信息只提供给具有适当权限的有效用户。

这就是如何使用Vault管理Go应用程序的秘密

虽然这不是对使用Vault管理秘密的深入研究,而且只涵盖了密钥价值引擎,但它仍然是学习Vault的一个良好起点。请看一下文档并玩一下代码。我很乐意看到你用Go和Vault建立的东西。