Posts Autorização de infraestrutura como código
Post
Cancel

Autorização de Infraestrutura como Código

Uma imagem que possui um simbolo que representa o código fonte sendo implementado na nuvem e um cadeado representando segurança no meio do processo

Entenda os problemas de não possuir um processo de autorização em pipelines de infraestrutra como código e veja uma solução com um bom custo-benefício para mitigar esses problemas.

Introdução​

Autorização é sempre um tema delicado, pois envolve a segurança de um software ou processo. Porém, quando falamos de infraestrutura como código, ter guardrails se torna ainda mais importante, pois é possível que um desenvolvedor cometa um erro e acabe expondo dados sensíveis ou até mesmo derrubando um ambiente inteiro.

Pensando em um ambiente corporativo dinâmico, o processo de gerenciamento de software moderno inclui a execução de pipelines de CI/CD para implantação de alterações, tanto de infraestrutura quanto de aplicação. Essas alterações podem ser feitas por desenvolvedores, engenheiros DevOps ou até mesmo por um usuário mal-intencionado.

Com o objetivo de mitigar esses riscos, é possível utilizar ferramentas de autorização que podem ser usadas para validar infraestrutura como código, como por exemplo o Open Policy Agent. O OPA é um motor de políticas open source, leve e de propósito geral, capaz de ser utilizado em diversos cenários como: autorização de APIs e microserviços, admission controller no kubernetes, autorização de tópicos e filas e também autorização de infraestrutura como código.

O problema​

Digamos que você possua em sua empresa um processo de build e deploy, automatizado e bem definido. Para fins demonstrativos, vamos supor que sua pipeline de CI/CD execute usando o Github Actions e esse workflow específico seja responsável de realizar a criação de um bucket s3 em sua conta AWS utilizando Terraform como IaC. Considere este repositório como referência e vamos analisar a estrutura do projeto:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── .github
│   └── workflows
│       └── deploy.yml
└── iac
    ├── .gitignore
    ├── backend.tf
    ├── main.tf
    ├── outputs.tf
    ├── provider.tf
    ├── terraform.tfvars
    └── variables.tf

O diretório .github é responsável por abrigar os workflows de CI/CD, que contem o arquivo de workflow deploy.yml. O diretório iac é responsável por abrigar os arquivos de configuração do Terraform, que são os arquivos backend.tf, main.tf, outputs.tf, provider.tf, terraform.tfvars e variables.tf.

O arquivo deploy.yml é responsável por executar o deploy do bucket s3 na conta AWS. Este workflow é executado quando um push é realizado na branch main. Veja abaixo o conteúdo do arquivo deploy.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
name: iac authz example workflow
on:
  push:
    branches:
      - main
    paths-ignore:
      - ".gitignore"
      - "README.md"
permissions:
  id-token: write
  contents: read
env:
  TF_BACKEND: iac-authz-example-tf
  AWS_REGION: $
defaults:
  run:
    shell: bash
jobs:
  terraform-backend:
    name: Ensure terraform backend
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: $
          aws-region: $
      - name: Create terraform backend
        run: |
          if [[ -z $(aws s3api list-buckets --query 'Buckets[?Name==`$`]' --output text) ]]; then
            aws s3 mb s3://$
          fi
          aws s3api head-object --bucket bucket-name --key terraform.tfstate || NOT_EXIST=true
          if [ $NOT_EXIST ]; then
            aws s3api put-object --bucket $ --key terraform.tfstate
          fi
  terraform:
    name: Deploy infrastructure
    needs: terraform-backend
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: iac
    steps:
      - name: Checkout repo
        uses: actions/checkout@v3
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: $
          aws-region: $
      - name: Setup terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_wrapper: false
      - name: Terraform fmt
        run: terraform fmt -check
      - name: Terraform init
        run: |
          terraform init -upgrade \
            -backend-config="bucket=$" \
            -backend-config="key=terraform.tfstate" \
            -backend-config="region=$"
      - name: Terraform validate
        run: terraform validate -no-color
      - name: Terraform plan
        run: terraform plan -no-color -var-file terraform.tfvars
      - name: Terraform apply
        run: terraform apply -auto-approve -var-file terraform.tfvars
      - name: Terraform outputs
        id: tf-outputs
        run: |
          echo "bucket-arn=`terraform output -raw bucket_arn`" >> $GITHUB_OUTPUT

No arquivo deploy.yml, temos dois jobs, o primeiro é responsável por criar o bucket s3 que será utilizado como backend do Terraform, enquanto o segundo é responsável por executar o deploy do do terraform do projeto em si, neste caso o bucket s3.

Agora, vamos pensar em algumas regras básicas de segurança quando tratamos buckets s3 corporativamente:

  • Necessidade de garantir que os buckets sejam privados
  • Necessidade de garantir que os buckets sejam criptografados
  • Necessidade de garantir que os buckets tenham versionamento habilitado

Podemos denominar que estas regras em conjunto caracterizam uma baseline de segurança especificamente para o recurso s3 da AWS. Não possuir uma autorização na pipeline de infraestrutura que garanta que essas regras estejam sendo aplicadas pode resultar em arquivos sensíveis sendo expostos, perda de arquivos, danos de imagem, danos financeiros e entre outros.

Aplicando autorização de IaC​

Com o entendimento da estrutura do projeto e o problema exposto, vamos partir para uma solução que mitiga estes riscos. A ideia é ter um step no workflow onde podemos validar o plano do terraform a ser executado e aplicar a baseline de segurança.

Política de autorização​

O primeiro passo aqui seria expressar a baseline de segurança do s3 que definimos acima em políticas na linguagem rego, por questões de simplicidade, iremos embutir as políticas de segurança no mesmo repositório do exemplo. Entretanto tenha em mente que em um ambiente corporativo, as políticas de segurança devem ser mantidas em um repositório separado, para que possam ser versionadas, auditadas e fora do alcance de alterações não autorizadas.

Vamos criar um diretório chamado policy e dentro dele um arquivo chamado main.rego com o seguinte conteúdo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import data.baseline.aws.common
import data.baseline.aws.s3
import future.keywords.if

default allow := false

allow if {
    s3.baseline_valid
}

result["allowed"] := allow
result["violations"] := s3.violations

E também vamos criar um diretório chamado baseline e dentro dele um diretório chamado aws e um arquivo chamado s3.rego que contem a baseline de segurança do s3, com o conteúdo a seguir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package baseline.aws.s3

import future.keywords.if
import input as tfplan

baseline_valid if {
    s3_bucket_public_access__changes := [r | r := tfplan.resource_changes[_]; r.type == "aws_s3_bucket_public_access_block"]
    s3_bucket_server_side_encryption_configuration__changes := [r | r := tfplan.resource_changes[_]; r.type == "aws_s3_bucket_server_side_encryption_configuration"]
    s3_bucket_versioning__changes := [r | r := tfplan.resource_changes[_]; r.type == "aws_s3_bucket_versioning"]
    s3__public_access_disabled(s3_bucket_public_access__changes)
    s3__bucket_cryptography_enabled(s3_bucket_server_side_encryption_configuration__changes)
    s3__bucket_versioning_enabled(s3_bucket_versioning__changes)
}

violations["S3 - Bucket should block all public access"] {
    s3_bucket_public_access__changes := [r | r := tfplan.resource_changes[_]; r.type == "aws_s3_bucket_public_access_block"]
    not s3__public_access_disabled(s3_bucket_public_access__changes)
}

violations["S3 - Bucket should be encrypted"] {
    s3_bucket_server_side_encryption_configuration__changes := [r | r := tfplan.resource_changes[_]; r.type == "aws_s3_bucket_server_side_encryption_configuration"]
    not s3__bucket_cryptography_enabled(s3_bucket_server_side_encryption_configuration__changes)
}

violations["S3 - Bucket should be versioned"] {
    s3_bucket_versioning__changes := [r | r := tfplan.resource_changes[_]; r.type == "aws_s3_bucket_versioning"]
    not s3__bucket_versioning_enabled(s3_bucket_versioning__changes)
}

########################################################
# Baseline: S3 - Bucket should block all public access #
########################################################
s3__public_access_disabled(s3_bucket_public_access__changes) if {
    s3_bucket_public_access__changes[_].change.after.block_public_acls == true
    s3_bucket_public_access__changes[_].change.after.block_public_policy == true
    s3_bucket_public_access__changes[_].change.after.ignore_public_acls == true
    s3_bucket_public_access__changes[_].change.after.restrict_public_buckets == true
}

#############################################
# Baseline: S3 - Bucket should be encrypted #
#############################################
s3__bucket_cryptography_enabled(s3_bucket_server_side_encryption_configuration__changes) if {
    s3_bucket_server_side_encryption_configuration__changes[_].change.after.rule[_].apply_server_side_encryption_by_default[_].sse_algorithm == "AES256"
}

s3__bucket_cryptography_enabled(s3_bucket_server_side_encryption_configuration__changes) if {
    s3_bucket_server_side_encryption_configuration__changes[_].change.after.rule[_].apply_server_side_encryption_by_default[_].sse_algorithm == "aws:kms"
    s3_bucket_server_side_encryption_configuration__changes[_].change.after.rule[_].apply_server_side_encryption_by_default[_].kms_master_key_id != ""
}

s3__bucket_cryptography_enabled(s3_bucket_server_side_encryption_configuration__changes) if {
    s3_bucket_server_side_encryption_configuration__changes[_].change.after.rule[_].apply_server_side_encryption_by_default[_].sse_algorithm == "aws:kms:dsse"
    s3_bucket_server_side_encryption_configuration__changes[_].change.after.rule[_].apply_server_side_encryption_by_default[_].kms_master_key_id != ""
}

#############################################
# Baseline: S3 - Bucket should be versioned #
#############################################
s3__bucket_versioning_enabled(s3_bucket_versioning__changes) if {
    lower(s3_bucket_versioning__changes[_].change.after.versioning_configuration[_].status) == "enabled"
}

Com a política escrita, agora precisamos integrar ao workflow.

Integração com o workflow​

O próximo passo é integrar as políticas criadas no passo anterior com o workflow do Github Actions e forçar a falha caso alguma das políticas não seja atendida. Para isso, precisamos primeiramente realizar a instalação do OPA no workflow, portanto, vamos utilizar o OPA Setup Github Action, conforme abaixo:

1
2
3
4
- name: Setup OPA
  uses: open-policy-agent/setup-opa@v2
  with:
    version: latest

Em seguida a ideia é obter o resultado enviado pela política de autorização, interpretar e falhar a pipeline caso a política sinalize, conforme a seguir:

1
2
3
4
5
6
7
8
9
10
11
12
- name: Terraform Authz
  run: |
    RESULT=`opa exec --decision main/result --bundle ../policy/ tfplan.json`
    ALLOWED=`echo $RESULT | jq -r '.result[0].result.allowed'`
    VIOLATIONS=`echo $RESULT | jq -r '.result[0].result.violations'`
    if [ "$ALLOWED" == "true" ]; then
      echo "Terraform authz success"
    else
      echo "Terraform authz failed"
      echo "Security violations: $VIOLATIONS"
      exit 1
    fi

Validando a solução​

Primeiramente, vamos realizar o teste quebrando alguma regra da baseline de segurança. Para isto iremos manipular o arquivo main.tf quebrando as regras a seguir:

  • Necessidade de garantir que os buckets sejam privados
  • Necessidade de garantir que os buckets tenham versionamento habilitado

Portanto o código ficaria desta maneira:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
resource "aws_s3_bucket" "this" {
  bucket = "my-tf-test-bucket-${random_id.this.hex}"
  tags = var.tags
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Disabled" # quebrando a baseline
  }
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket = aws_s3_bucket.this.id
  block_public_acls       = false # quebrando a baseline
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "random_id" "this" {
  byte_length = 8
}

A execução da pipeline se dará desta maneira:

Imagem mostra uma execução de pipeline do github actions com falha no passo Terraform Authz devido as violações encontradas no terraform

Note que a policy executou, identificou problemas de autorização no plano do terraform e impediu que a esteira continuasse com a criação dos recursos. Caso os recursos de acesso público e versionamento não fossem declarados, a pipeline também iria falhar.

Por fim, vamos testar a autorização de IaC com sucesso, para isso, vamos corrigir os problemas de segurança no arquivo main.tf e executar a pipeline novamente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
resource "aws_s3_bucket" "this" {
  bucket = "my-tf-test-bucket-${random_id.this.hex}"
  tags = var.tags
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Enabled" # corrigir problema de segurança
  }
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket = aws_s3_bucket.this.id
  block_public_acls       = true # corrigir problema de segurança
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "random_id" "this" {
  byte_length = 8
}

A execução da pipeline se dará desta maneira:

A imagem mostra uma execução de pipeline no github actions com sucesso

Conclusão​

Neste artigo foi apresentado como é possível autorizar infraestrutura como código utilizando o OPA e Github Actions. A solução abordada tem um investimento de tempo relativamente baixo, desta maneira, tem um ótimo custo-benefício para empresas que estão iniciando ou querem iniciar a jornada de autorização de infraestrutura como código.

Esse post está licenciado sob CC BY 4.0 pelo autor.