TLDR: o OpenTofu agora conta com criptografia de State files (e de Plan files também!) sem depender do backend remoto. O Terraform não - e talvez nunca suporte em sua versão gratuita.
É de extrema importância conhecer bem as ferramentas que você pretende usar para ser capaz de desenhar bons fluxos de trabalho e, especialmente, mapear os riscos envolvidos.
Quem usa Terraform já está familiarizado com a relevância do State File e que é de extrema importância garantir não só a resiliência do arquivo como também o controle de acesso.
No post abaixo, vamos fazer um estudo de caso demonstrando os problemas de ter um arquivo de State File em texto claro disponível em buckets S3.
Vamos supor que você criou uma variável no AWS Secrets Manager com Terraform:
$ aws sts get-caller-identity
{
"UserId": "AIDA2UC3CSEZOOZQXHZCN",
"Account": "730335449394",
"Arn": "arn:aws:iam::730335449394:user/cloud_user"
}
$ aws secretsmanager get-secret-value --secret-id senha_root
{
"ARN": "arn:aws:secretsmanager:us-east-1:730335449394:secret:senha_root-qiNJDs",
"Name": "senha_root",
"VersionId": "terraform-20240701215533811100000001",
"SecretString": "z0mgp4ssw0rd",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2024-07-01T18:55:32.978000-03:00"
}
Um usuário hacker não vai ter acesso à variável, a menos que alguém dê acesso explícito para ele:
$ aws sts get-caller-identity
{
"UserId": "AIDA2UC3CSEZLYNWRDSTL",
"Account": "730335449394",
"Arn": "arn:aws:iam::730335449394:user/hacker"
}
$ aws secretsmanager get-secret-value --secret-id senha_root
An error occurred (AccessDeniedException) when calling the GetSecretValue operation: User: arn:aws:iam::730335449394:user/hacker is not authorized to perform: secretsmanager:GetSecretValue on resource: senha_root because no identity-based policy allows the secretsmanager:GetSecretValue action
Observe que o erro é claramente permissão:
no identity-based policy allows the secretsmanager:GetSecretValue action
Caso o hacker consiga comprometer um usuário que conte com acesso ao Secrets Manager, ainda é possível usar criptografia de uma chave KMS específica (e não a padrão do Secrets Manager), o que adiciona uma segunda camada de segurança:
# Agora o hacker conseguiu comprometer um usuário com acesso ao SM!
# Isso deu a ele a chance de acessar secrets menos importantes:
$ aws secretsmanager get-secret-value --secret-id senha_menos_importante
{
"ARN": "arn:aws:secretsmanager:us-east-1:730335449394:secret:senha_menos_importante-Y4reR7",
"Name": "senha_menos_importante",
"VersionId": "e919b63c-6937-44ca-81b9-b8b2172c6b33",
"SecretString": "{\"SENHA\":\"menos_importante\"}",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2024-07-01T20:22:02.007000-03:00"
}
# A senha_root, entretanto, está criptografada com uma chave KMS não padrão:
$ aws secretsmanager get-secret-value --secret-id senha_root
An error occurred (AccessDeniedException) when calling the GetSecretValue operation: Access to KMS is not allowed
Aqui, a mensagem de erro mudou:
Access to KMS is not allowed
Para acessar a senha importante, é necessário não só acesso ao serviço Secrets Manager, mas também ao serviço de gestão de chaves KMS, que normalmente conta com Resource Policies mais restritas de acordo com a finalidade.
Só que... Geralmente guardamos arquivos de Terraform State em S3!
E se o hacker comprometer um usuário com acesso a buckets S3?
Acessando arquivos de Terraform State
Um arquivo Terraform State é um JSON escrito em texto claro, sem qualquer proteção aos valores salvos.
Podemos até escrever o código Terraform da seguinte maneira:
$ cat variables.tf
variable "senha_root" {
type = string
description = "Senha de root mais importante que temos"
sensitive = true
nullable = false
}
Note o atributo sensitive!
Podemos também garantir que o parâmetro só é conhecido em tempo de execução de código por meio de passagem de parâmetro do usuário:
# Um formulário permitiu preencher a variável SENHA antes da execução:
$ terraform apply -var "senha_root=$SENHA" -auto-approve
Só que, na prática, o atributo sensitive apenas garante que o atributo não será exibido na saída da execução do comando ou em mensagens de log, como descrito aqui:
Terraform will then redact these values in the output of Terraform commands or log messages.
No mesmo link acima, um alerta importante em outra seção:
When you run Terraform commands with a local state file, Terraform stores the state as plain text, including variable values, even if you have flagged them as sensitive. Terraform needs to store these values in your state so that it can tell if you have changed them since the last time you applied your configuration.
Traduzindo: o Terraform salva o State File em texto claro!
E mais abaixo:
Marking variables as sensitive is not sufficient to secure them.
Como eles mesmos descrevem:
You must also keep them secure while passing them into Terraform configuration...
...como fizemos com o formulário, mas...
...and protect them in your state file.
Claro que você pode usar as versões pagas para resolver seu problema:
HCP Terraform and Terraform Enterprise manage and share sensitive values, and encrypt all variable values before storing them.
Caso contrário, você está sem sorte, como na sequência de comandos abaixo:
$ aws s3api list-buckets --query 'Buckets[*].Name' --output text
devsres-terraform-state-storage
$ aws s3 ls --recursive s3://devsres-terraform-state-storage/
2024-07-01 20:30:15 3987 terraform/state/senha_root
$ aws s3 cp s3://devsres-terraform-state-storage/terraform/state/senha_root /tmp/
download: s3://devsres-terraform-state-storage/terraform/state/senha_root to /tmp/senha_root
# A maioria das pessoas simplesmente usaria um grep mesmo:
$ jq -r '.resources[].instances[].attributes | select(.secret_string != null).secret_string' /tmp/senha_root
z0mgM1nh4p@ssw0rd!
Opentofu terraform state encryption
Só que isso é se estivermos falando do Terraform.
O OpenTofu é um projeto software livre derivado do Terraform após a mudança de licença da Hashicorp para BSL.
Em vez de ser um simples "fork", o OpenTofu trouxe novas funcionalidades que respondem ao anseio de muitas pessoas da comunidade, na prática deixando os códigos agora nem sempre 100% compatíveis!
Por exemplo, é possível criptografar o Terraform State com uma passphrase ou com um serviço de chaves como o KMS sem precisar contar com funcionalidades nativas do S3!
Como fazer isso?
Bem, aí é necessário ler a documentação. Essa funcionalidade está disponível desde a versão 1.7.0 lançada em abril de 2024.
O exemplo abaixo é suficiente para mostrar a criptografia com passphrase:
$ cat terraform.tf
terraform {
backend "s3" {
bucket = "devsres-terraform-state-storage"
key = "tofu/state/senha_root"
region = "us-east-1"
# encrypt = true
# kms_key_id = "arn:aws:kms:us-east-1:730335449394:key/9b8acab0-df09-4c2b-81ab-7dd33d2a4ba2"
}
encryption {
key_provider "pbkdf2" "senha_de_state" {
passphrase = "wow!criptografia!"
}
method "aes_gcm" "protege" {
keys = key_provider.pbkdf2.senha_de_state
}
state {
method = method.aes_gcm.protege
enforced = true
}
}
}
Ao usar o código acima, este é o State File disponível no bucket S3:
$ aws s3 cp s3://devsres-terraform-state-storage/tofu/state/senha_root /tmp/
t download: s3://devsres-terraform-state-storage/tofu/state/senha_root to ../../../../../tmp/senha_root
$ cat /tmp/senha_root
{"serial":1,"lineage":"5d8b4611-05a3-1f8a-404a-11e1b0693e8f","meta":{"key_provider.pbkdf2.senha_de_state":"eyJzYWx0IjoiN3d4VmgxcHF3S01EQ0FzRXpqZ0xnU0w3bkJGbmFSd2pmSGVwWm45VTlkOD0iLCJpdGVyYXRpb25zIjo2MDAwMDAsImhhc2hfZnVuY3Rpb24iOiJzaGE1MTIiLCJrZXlfbGVuZ3RoIjozMn0="},"encrypted_data":"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","encryption_version":"v0"}
O arquivo está criptografado e indisponível para acesso direto.
Ah, e se você tentar usar esse bloco de código com o Terraform, ele trará um erro:
$ terraform init -migrate-state
...
╷
│ Error: Unsupported block type
│
│ on main.tf line 10, in terraform:
│ 10: encryption {
│
│ Blocks of type "encryption" are not expected here.
╵
Extra: Mitigação na AWS com KMS
Uma pessoa mais sagaz poderia argumentar: "ah, é só usar a chave KMS para criptografar também o seu State File"!
$ cat
terraform {
backend "s3" {
bucket = "devsres-terraform-state-storage"
key = "terraform/state/senha_root"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:730335449394:key/9b8acab0-df09-4c2b-81ab-7dd33d2a4ba2"
}
}
E é verdade, isso mitiga o acesso:
$ aws s3 cp s3://devsres-terraform-state-storage/terraform/state/senha_root /tmp/
download failed: s3://devsres-terraform-state-storage/terraform/state/senha_root to ../../tmp/senha_root An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:iam::730335449394:user/hacker is not authorized to perform: kms:Decrypt on resource: arn:aws:kms:us-east-1:730335449394:key/9b8acab0-df09-4c2b-81ab-7dd33d2a4ba2 because no identity-based policy allows the kms:Decrypt action
Mas nem todo backend suporta esta funcionalidade. Salvo engano, não é possível usar com Azure Blobs; outros backends e usuários on-premises também ficam sem opção.