Construindo infraestrutura como código com Open Policy Agent e GitHub Actions

Itaú Tech
ItauTech
Published in
7 min readJul 2, 2024

Por Daniel De Padua Ferreira e Matheus Duarte Rabello, Team Members na Comunidade da Segurança da Informação

Parte II

Agora que você já explorou importância do Open Policy Agent (OPA) na gestão de autorizações e entendeu seus benefícios, neste artigo iremos mergulhar em um hands-on que demonstrará na prática a importância da segurança em Infraestrutura como código.

Caso de uso

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 que esse workflow específico seja responsável por 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:

.
├── .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 contém o arquivo de workflow deploy.yml. Já 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:

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: ${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_role}
aws-region: ${aws_region}
- name: Create terraform backend
run: |
if [[ -z $(aws s3api list-buckets --query 'Buckets[?Name==`${aws_s3_bucket_name}`]' --output text) ]]; then
aws s3 mb s3://${aws_s3_backend_path}
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_role}
aws-region: ${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=${aws_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 terraform do projeto em si (neste caso, o bucket s3).

Agora, vamos pensar em algumas regras básicas de segurança para tratarmos 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​ e Política de autorização​

Com o entendimento da estrutura do projeto e o problema exposto, vamos partir para uma solução que mitigue 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.

O primeiro passo será expressar a baseline de segurança do s3 que definimos anteriormente 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 e auditadas e para que estejam 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:

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

Além disso, criaremos um diretório chamado baseline. Dentro dele, um diretório chamado aws e um arquivo chamado s3.rego, que contém a baseline de segurança do s3, com o conteúdo a seguir:

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.

Realizando a 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. Abaixo, podemos visualizar o Diagrama de fluxo da esteira de entrega contínua dos recursos cloud usando terraform com um passo destacado onde é feita autorização do código enviado antes de realizar a entrega do recurso na nuvem.

Vamos utilizar o OPA Setup Github Action, conforme abaixo:

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:

- 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

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.

O código ficaria desta maneira:

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:

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:

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:

Conclusão​

Neste artigo, apresentamos como podemos autorizar infraestrutura como código utilizando o OPA e GitHub Actions. A solução abordada tem um investimento de tempo relativamente baixo, o que significa um ótimo custo-benefício para empresas que estão iniciando ou querem iniciar a jornada de autorização de infraestrutura como código.

--

--