由于现代软件是如此复杂,它需要使用秘密和机密信息,如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建立的东西。