Terraform, OpenTofu e criptografia de estado

Marcelo Andrade - Jul 2 - - Dev Community

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
    }

  }
}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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.
╵
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .