如何保护Terraform中的敏感数据
作为 Write for DOnations 计划的一部分,作者选择了 Free and Open Source Fund 来接受捐赠。
介绍
Terraform 提供自动化以在云中配置您的基础设施。 为此,Terraform 向云提供商(和其他提供商)进行身份验证,以部署资源并执行计划的操作。 但是,Terraform 身份验证所需的信息非常有价值,通常是敏感信息,您应该始终保密,因为它会解锁对您服务的访问。 例如,您可以将数据库用户的 API 密钥或密码视为敏感数据。
如果恶意第三方获取敏感信息,他们将能够通过将自己呈现为已知的受信任用户来破坏安全系统。 反过来,他们将能够修改、删除和替换在获得的密钥范围内可用的资源和服务。 为了防止这种情况发生,必须正确保护您的项目并保护其状态文件,该文件存储所有项目机密。
默认情况下,Terraform 以未加密的 JSON 形式将状态文件存储在本地,允许任何有权访问项目文件的人读取机密信息。 虽然解决方案是限制对磁盘上文件的访问,但另一种选择是将状态远程存储在自动加密数据的后端,例如 DigitalOcean Spaces。
在本教程中,您将在执行期间将敏感数据隐藏在输出中,并将您的状态存储在安全的云对象存储中,该存储对静态数据进行加密。 您将在本教程中使用 DigitalOcean Spaces 作为您的云对象存储。 您还将学习如何将变量标记为敏感变量,以及探索 tfmask,这是一个用 Go 编写的开源程序,可动态审查 Terraform 执行日志输出中的值。
先决条件
- DigitalOcean 个人访问令牌,您可以通过 DigitalOcean 控制面板创建它。 您可以在 DigitalOcean 产品文档中找到说明,如何创建个人访问令牌。
- Terraform 安装在您的本地计算机上,并使用 DigitalOcean 提供程序设置了一个项目。 完成 How To Use Terraform with DigitalOcean 教程的 Step 1 和 Step 2,并确保将项目文件夹命名为
terraform-sensitive
,而不是loadbalance
。 在 Step 2 期间,不要包含pvt_key
变量和 SSH 密钥资源。 - 带有 API 密钥(访问和秘密)的 DigitalOcean 空间。 要了解如何创建 DigitalOcean Space 和 API 密钥,请参阅教程 如何创建 DigitalOcean Space 和 API 密钥。
注意: 本教程专门用 Terraform 1.0.2
测试过。
将输出标记为 sensitive
在此步骤中,您将通过将 sensitive
参数设置为 true
来隐藏代码中的输出。 当秘密值是您无限期存储的 Terraform 输出的一部分,或者您需要在团队之外共享输出日志以进行分析时,这很有用。
假设您位于作为先决条件的一部分创建的 terraform-sensitive
目录中,您将定义一个 Droplet 和一个显示其 IP 地址的输出。 您将把它存储在一个名为 droplets.tf
的文件中,因此通过运行创建并打开它进行编辑:
nano droplets.tf
添加以下行:
terraform-sensitive/droplets.tf
resource "digitalocean_droplet" "web" { image = "ubuntu-20-04-x64" name = "web-1" region = "fra1" size = "s-1vcpu-1gb" } output "droplet_ip_address" { value = digitalocean_droplet.web.ipv4_address }
此代码将在 fra1
区域部署一个名为 web-1
的 Droplet,在 1GB RAM 和一个 CPU 内核上运行 Ubuntu 20.04。 在这里,您为 droplet_ip_address
输出提供了一个值,您将在 Terraform 日志中收到该值。
要部署此 Droplet,请通过运行以下命令执行代码:
terraform apply -var "do_token=${DO_PAT}"
Terraform 将采取的行动如下:
OutputTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.web will be created + resource "digitalocean_droplet" "web" { + backups = false + created_at = (known after apply) + disk = (known after apply) + id = (known after apply) + image = "ubuntu-20-04-x64" + ipv4_address = (known after apply) + ipv4_address_private = (known after apply) + ipv6 = false + ipv6_address = (known after apply) + ipv6_address_private = (known after apply) + locked = (known after apply) + memory = (known after apply) + monitoring = false + name = "web-1" + price_hourly = (known after apply) + price_monthly = (known after apply) + private_networking = (known after apply) + region = "fra1" + resize_disk = true + size = "s-1vcpu-1gb" + status = (known after apply) + urn = (known after apply) + vcpus = (known after apply) + volume_ids = (known after apply) + vpc_uuid = (known after apply) } Plan: 1 to add, 0 to change, 0 to destroy. ...
出现提示时输入 yes
。 输出将类似于以下内容:
Outputdigitalocean_droplet.web: Creating... ... digitalocean_droplet.web: Creation complete after 40s [id=216255733] Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Outputs: droplet_ip_address = your_droplet_ip_address
您会发现 IP 地址在输出中。 如果您要与其他人共享此输出,或者由于自动化部署过程而将其公开可用,则采取措施在输出中隐藏此数据非常重要。
要审查它,您需要将 droplet_ip_address
输出的 sensitive
属性设置为 true
。
打开droplets.tf
进行编辑:
nano droplets.tf
添加突出显示的行:
terraform-sensitive/droplets.tf
resource "digitalocean_droplet" "web" { image = "ubuntu-20-04-x64" name = "web-1" region = "fra1" size = "s-1vcpu-1gb" } output "droplet_ip_address" { value = digitalocean_droplet.web.ipv4_address sensitive = true }
完成后保存并关闭文件。
通过运行再次应用项目:
terraform apply -var "do_token=${DO_PAT}"
输出将是:
Outputdigitalocean_droplet.web: Refreshing state... [id=216255733] ... Apply complete! Resources: 0 added, 0 changed, 0 destroyed. Outputs: droplet_ip_address = <sensitive>
您现在已经明确地审查了 IP 地址——输出的值。 在 Terraform 日志位于公共空间中,或者您希望它们保持隐藏但不从代码中删除它们的情况下,审查输出非常有用。 您还需要审查包含密码和 API 令牌的输出,因为它们也是敏感信息。
您现在已经通过将定义的输出标记为 sensitive
来隐藏它们的值。 您现在将看到如何将变量标记为敏感。
将变量标记为 sensitive
与输出类似,变量也可以标记为敏感。 由于您只定义了一个变量(do_token
),请打开 provider.tf
进行编辑:
nano provider.tf
将 do_token
变量修改为如下所示:
terraform-sensitive/provider.tf
terraform { required_providers { digitalocean = { source = "digitalocean/digitalocean" version = "~> 2.0" } } } variable "do_token" { sensitive = true } provider "digitalocean" { token = var.do_token }
完成后,保存并关闭文件。 do_token
变量现在被认为是敏感的。
要尝试输出敏感变量,您将在 droplets.tf
中定义一个新输出:
nano droplets.tf
在末尾添加以下行:
terraform-sensitive/droplets.tf
output "dotoken" { value = var.do_token }
保存并关闭文件。 然后,尝试通过运行应用配置:
terraform apply -var "do_token=${DO_PAT}"
您将收到与此类似的错误消息:
Output╷ │ Error: Output refers to sensitive values │ │ on droplets.tf line 13: │ 13: output "dotoken" { │ │ To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires │ that any root module output containing sensitive data be explicitly marked as sensitive, to confirm your intent. │ │ If you do intend to export this data, annotate the output value as sensitive by adding the following argument: │ sensitive = true ╵
这个错误意味着敏感变量不能显示在非敏感输出中,以防止信息泄露。 但是,您可以通过将输出值包装为 nonsensitive
来强制显示它们,如下所示:
terraform-sensitive/droplets.tf
... output "dotoken" { value = nonsensitive(var.do_token) }
nonsensitive
重置变量的灵敏度首选项,允许显示。 这应该谨慎使用,并且仅当输出是敏感变量的不可逆导数时。
您现在已经了解了如何将变量标记为敏感,以及如何覆盖该首选项。 在下一步中,您将配置 Terraform 以将项目的状态存储在加密的云中,而不是本地。
将状态存储在加密的远程后端
状态文件存储有关您部署的基础架构的所有信息,包括其所有内部关系和机密。 默认情况下,它以明文形式存储在本地磁盘上。 将其远程存储在云中可提供更高级别的安全性。 如果云存储服务支持静态加密,它将始终以加密状态存储状态文件,这样潜在的攻击者将无法从中收集信息。 远程存储加密状态文件与将输出标记为 sensitive
不同——这样,所有机密都安全地存储在云中,这只会改变 Terraform 存储数据的方式,而不是在显示时。
您现在将配置您的项目以将状态文件存储在 DigitalOcean Space 中。 因此,它将被静态加密并在传输中使用 TLS 进行保护。
默认情况下,Terraform 状态文件名为 terraform.tfstate
,位于每个初始化目录的根目录中。 您可以通过运行查看其内容:
cat terraform.tfstate
该文件的内容将与此类似:
terraform 敏感/terraform.tfstate
{ "version": 4, "terraform_version": "1.0.2", "serial": 3, "lineage": "16362bdb-2ff3-8ac7-49cc-260f3261d8eb", "outputs": { "droplet_ip_address": { "value": "...", "type": "string", "sensitive": true } }, "resources": [ { "mode": "managed", "type": "digitalocean_droplet", "name": "web", "provider": "provider[\"registry.terraform.io/digitalocean/digitalocean\"]", "instances": [ { "schema_version": 1, "attributes": { "backups": false, "created_at": "2021-07-11T06:16:51Z", "disk": 25, "id": "254368889", "image": "ubuntu-20-04-x64", "ipv4_address": "...", "ipv4_address_private": "10.135.0.3", "ipv6": false, "ipv6_address": "", "locked": false, "memory": 1024, "monitoring": false, "name": "web-1", "price_hourly": 0.00744, "price_monthly": 5, "private_networking": true, "region": "fra1", "resize_disk": true, "size": "s-1vcpu-1gb", "ssh_keys": null, "status": "active", "tags": [], "urn": "do:droplet:254368889", "user_data": null, "vcpus": 1, "volume_ids": [], "vpc_uuid": "fc52519c-dc84-11e8-8b13-3cfdfea9f160" }, "sensitive_attributes": [], "private": "..." } ] } ] }
状态文件包含您部署的所有资源,以及所有输出及其计算值。 获得对这个文件的访问权足以危及整个部署的基础设施。 为防止这种情况发生,您可以将其加密存储在云中。
Terraform 支持多个后端,它们是状态的存储和检索机制。 例如:local
用于本地存储,pg
用于 Postgres 数据库,s3
用于 S3 兼容存储,您将使用它们连接到您的空间。
后端配置在主 terraform
块下指定,当前位于 provider.tf
中。 通过运行打开它进行编辑:
nano provider.tf
添加以下行:
terraform-sensitive/provider.tf
terraform { required_providers { digitalocean = { source = "digitalocean/digitalocean" version = "~> 2.0" } } backend "s3" { key = "state/terraform.tfstate" bucket = "your_space_name" region = "us-west-1" endpoint = "https://spaces_endpoint" skip_region_validation = true skip_credentials_validation = true skip_metadata_api_check = true } } variable "do_token" {} provider "digitalocean" { token = var.do_token }
s3
后端块首先指定key
,即Terraform状态文件在Space上的位置。 传入 state/terraform.tfstate
表示将其存储为 state
目录下的 terraform.tfstate
。
endpoint
参数告诉 Terraform 空间所在的位置,bucket
定义要连接的确切空间。 skip_region_validation
和 skip_credentials_validation
禁用不适用于 DigitalOcean Spaces 的验证。 请注意,region
必须设置为符合标准的值(例如 us-west-1
),它不引用空格。
请记住输入您的存储桶名称和 Spaces 端点,包括区域,您可以在 Space 的 Settings 选项卡中找到它们。 请注意,do_token
变量不再被标记为敏感。 完成自定义 endpoint
后,保存并关闭文件。
接下来,将 Space 的访问密钥和密钥放入环境变量中,以便您以后可以引用它们。 运行以下命令,将突出显示的占位符替换为您的键值:
export SPACE_ACCESS_KEY="your_space_access_key" export SPACE_SECRET_KEY="your_space_secret_key"
然后,通过运行以下命令将 Terraform 配置为使用 Space 作为其后端:
terraform init -backend-config "access_key=$SPACE_ACCESS_KEY" -backend-config "secret_key=$SPACE_SECRET_KEY"
-backend-config
参数提供了一种在运行时设置后端参数的方法,您在这里使用它来设置空格键。 系统会询问您是否希望将现有状态复制到云中,或者重新开始:
OutputInitializing the backend... Do you want to copy existing state to the new backend? Pre-existing state was found while migrating the previous "local" backend to the newly configured "s3" backend. No existing state was found in the newly configured "s3" backend. Do you want to copy this state to the new "s3" backend? Enter "yes" to copy and "no" to start with an empty state.
出现提示时输入 yes
。 输出的其余部分将类似于以下内容:
OutputSuccessfully configured the backend "s3"! Terraform will automatically use this backend unless the backend configuration changes. Initializing provider plugins... - Reusing previous version of digitalocean/digitalocean from the dependency lock file - Using previously-installed digitalocean/digitalocean v2.10.1 Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.
您的项目现在将其状态存储在您的空间中。 如果您收到错误,请仔细检查您是否提供了正确的密钥、终端节点和存储桶名称。
您的项目现在将状态存储在您的空间中。 本地状态文件已被清空,您可以通过显示其内容来检查:
cat terraform.tfstate
正如预期的那样,不会有输出。
您可以尝试修改 Droplet 定义并应用它来检查状态是否仍在正确管理。
打开droplets.tf
进行编辑:
nano droplets.tf
修改突出显示的行:
terraform-sensitive/droplets.tf
resource "digitalocean_droplet" "web" { image = "ubuntu-20-04-x64" name = "test-droplet" region = "fra1" size = "s-1vcpu-1gb" } output "droplet_ip_address" { value = digitalocean_droplet.web.ipv4_address sensitive = false }
您可以删除之前的 dotoken
输出。 保存并关闭文件,然后通过运行应用项目:
terraform apply -var "do_token=${DO_PAT}"
输出将类似于以下内容:
OutputTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place Terraform will perform the following actions: # digitalocean_droplet.web will be updated in-place ~ resource "digitalocean_droplet" "web" { id = "254368889" ~ name = "web-1" -> "test-droplet" tags = [] # (21 unchanged attributes hidden) } Plan: 0 to add, 1 to change, 0 to destroy. ...
出现提示时输入 yes
,Terraform 会将新配置应用到现有的 Droplet,这意味着它正在与存储其状态的空间正确通信:
Output... digitalocean_droplet.web: Modifying... [id=216419273] digitalocean_droplet.web: Still modifying... [id=216419273, 10s elapsed] digitalocean_droplet.web: Modifications complete after 12s [id=216419273] Apply complete! Resources: 0 added, 1 changed, 0 destroyed. Outputs: droplet_ip_address = your_droplet_ip_address
您已经为您的项目配置了 s3
后端,以便将在云中加密的状态存储在 DigitalOcean Space 中。 在下一步中,您将使用 tfmask
,该工具将动态审查 Terraform 日志中的所有敏感输出和信息。
在 CI/CD 环境中使用 tfmask
在本节中,您将下载 tfmask
并使用它来动态审查 Terraform 在执行命令时生成的整个输出日志中的敏感数据。 它将审查其值与您提供的 RegEx 表达式匹配的变量和参数。
如果参数和变量名称遵循某种模式(例如,包含单词 password
或 secret
),则可以动态匹配参数和变量名称。 与将输出标记为敏感相比,使用 tfmask
的优势在于它还会审查 Terraform 在执行时打印出的资源声明的匹配部分。 当执行日志可能是公开的时,您必须隐藏它们,例如在自动化 CI/CD 环境中,这通常会公开列出执行日志。
tfmask
的已编译二进制文件可在 GitHub 上的 发布页面 上获得。 对于 Linux,运行以下命令进行下载:
sudo curl -L https://github.com/cloudposse/tfmask/releases/download/0.7.0/tfmask_linux_amd64 -o /usr/bin/tfmask
通过运行将其标记为可执行文件:
sudo chmod +x /usr/bin/tfmask
tfmask
通过屏蔽名称与您指定的 RegEx 表达式匹配的所有变量的值,对 terraform plan
和 terraform apply
的输出起作用。 您将使用环境变量 TFMASK_VALUES_REGEX
和 TFMASK_CHAR
来提供正则表达式,以及替换实际值的字符。
您现在将使用 tfmask
审查 Terraform 将部署的 Droplet 的 name
和 ipv4_address
。 首先,您需要通过运行设置上述环境变量:
export TFMASK_CHAR="*" export TFMASK_VALUES_REGEX="(?i)^.*(ipv4_address|name).*$"
此正则表达式将匹配所有以 ipv4_address
或 name
开头的字符串(以及它们本身),并且不区分大小写。
为了让 Terraform 为你的 Droplet 计划一个动作,修改它的定义:
nano droplets.tf
修改 Droplet 的名称:
terraform-sensitive/droplets.tf
resource "digitalocean_droplet" "web" { image = "ubuntu-20-04-x64" name = "web" region = "fra1" size = "s-1vcpu-1gb" } output "droplet_ip_address" { value = digitalocean_droplet.web.ipv4_address sensitive = false }
保存并关闭文件。
因为您更改了 Droplet 的属性,Terraform 将在其输出中显示其完整定义。 计划配置,但将其通过管道传输到 tfmask
以根据正则表达式检查变量:
terraform plan -var "do_token=${DO_PAT}" | tfmask
您将收到类似于以下内容的输出:
Outputdigitalocean_droplet.web: Refreshing state... [id=216419273] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place Terraform will perform the following actions: # digitalocean_droplet.web will be updated in-place ~ resource "digitalocean_droplet" "web" { id = "254368889" ~ name = "**********************************" tags = [] # (21 unchanged attributes hidden) } Plan: 0 to add, 1 to change, 0 to destroy. ...
请注意,tfmask
使用您在 TFMASK_CHAR
环境变量中指定的字符审查了 name
、ipv4_address
和 ipv4_address_private
的值,因为它们匹配正则表达式。
Terraform 日志中的这种价值审查方式对于 CI/CD 非常有用,其中日志可能是公开的。 tfmask
的好处是您可以完全控制要审查的变量(使用正则表达式)。 您还可以指定要审查的关键字,这些关键字当前可能不存在,但您预计将来会使用。
您可以通过运行以下命令并在出现提示时输入 yes
来销毁已部署的资源:
terraform destroy -var "do_token=${DO_PAT}"
结论
在本文中,您使用了几种方法来隐藏和保护 Terraform 项目中的敏感数据。 第一个措施,使用 sensitive
隐藏输出和变量中的值,当只有日志可访问时很有用,但值本身可以保留在存储在磁盘上的状态中。
为了解决这个问题,您可以选择远程存储状态文件,这是您使用 DigitalOcean Spaces 实现的。 这使您可以使用静态加密。 您还使用了 tfmask
,这是一种在 terraform plan
和 terraform apply
期间审查变量值的工具(使用正则表达式匹配)。
您还可以查看 Hashicorp Vault 来存储机密和机密数据。 它可以与 Terraform 集成以在资源定义中 注入秘密 ,因此您将能够将您的项目与现有的 Vault 工作流程连接起来。 您可能想查看我们关于 如何在 DigitalOcean 上使用 Packer 和 Terraform 构建 Hashicorp Vault 服务器的教程。
本教程是 如何使用 Terraform 管理基础架构系列的一部分。 该系列涵盖了许多 Terraform 主题,从首次安装 Terraform 到管理复杂的项目。