← Back to list

AWS para Engenheiros de Dados (Parte 4): AWS STS + OIDC - Autenticação Federada e CI/CD

Como eliminar chaves AWS hardcoded no CI/CD usando STS e OIDC. GitHub Actions, Terraform e boto3 para pipelines de dados seguros na AWS.

Rafael Trindade · 2026-07-25 16:41 · 0 claps · 10.6 min read
#aws #engenharia-de-dados #devops #cloud-computing #segurança
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔓 · Open Source

AWS para Engenheiros de Dados (Parte 4): AWS STS + OIDC - Autenticação Federada e CI/CD sem Segredos

Bem-vindo(a) à série AWS para Engenheiros de Dados (Parte 4). Esta série nasceu das minhas anotações, estudos e experiências práticas explorando o ecossistema AWS sob a perspectiva da engenharia de dados. Mais do que aprender serviços específicos, a proposta é entender os fundamentos que sustentam arquiteturas seguras, escaláveis e preparadas para produção.

Ao longo dos próximos capítulos, vamos explorar os principais serviços da AWS conectando teoria e prática, desde os conceitos essenciais até cenários reais de arquitetura, operação, certificações e ambientes produtivos. A ideia é construir uma visão sólida da plataforma, entendendo não apenas como usar cada serviço, mas principalmente por que ele existe e quais problemas ele resolve.

No episódio de hoje, vamos desmistificar o AWS STS e o OIDC (OpenID Connect). Vamos entender como eliminar de vez a vulnerabilidade número um em pipelines de dados: chaves fixas (hardcoded) em servidores de CI/CD. Mais do que teoria, vamos construir uma arquitetura real de federação de identidades, mostrando como orquestrar deploys automáticos no GitHub Actions com credenciais dinâmicas, zero senhas estáticas e governança de nível enterprise.

“O pior segredo é aquele que você nem sabe que está vazando. Chaves AWS hardcoded em CI/CD são a vulnerabilidade número 1 em pipelines de dados. STS + OIDC resolve isso de forma elegante - sem nenhuma senha estática.”

1. O QUE É STS?

STS = Security Token Service

É o serviço da AWS que emite credenciais temporárias. Em vez de uma chave que nunca expira (AWS_ACCESS_KEY_ID), o STS gera um trio de credenciais que vivem por minutos ou horas:

Access Key ID     → começa com "ASIA..." (temporária)
Secret Access Key → gerada dinamicamente
Session Token     → prova que é temporária, obrigatório junto
Expiration        → timestamp de quando tudo vira lixo

2. O QUE É OIDC?

OIDC = OpenID Connect

É um protocolo de identidade baseado em OAuth 2.0. Permite que sistemas externos (GitHub Actions, GitLab CI, CircleCI, etc.) provem sua identidade para a AWS sem nenhuma chave estática.

Funciona assim: o GitHub sabe quem você é (repositório, branch, organização). Ele emite um JWT token assinado provando essa identidade. A AWS confia no GitHub como provedor de identidade e troca esse JWT por credenciais temporárias via STS.

3. O FLUXO COMPLETO - COMO CI/CD ACESSA AWS

Agora veja o fluxo completo com todos os participantes:

4. ARQUITETURA REAL - STS EM ENGENHARIA DE DADOS

Por que separar roles por contexto? Um PR aberto por qualquer contribuidor não deve ter poder de deploy. Você condiciona a role pelo branch.

5. EXEMPLOS PRÁTICOS

## Terraform - Configurando OIDC Provider + Roles

# ─── OIDC Provider (feito uma vez por conta AWS) ───────────────────

data "tls_certificate" "github" {
  url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  # GitHub emite tokens com este audience
  client_id_list = ["sts.amazonaws.com"]

  # Fingerprint do certificado TLS do GitHub
  thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint]
}

# ─── Role para deploy de pipeline de dados ─────────────────────────

resource "aws_iam_role" "github_deploy" {
  name = "github-oidc-data-deploy"

  # Trust Policy: quem pode assumir essa role
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            # Audience obrigatório
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com",
            # Usamos StringEquals aqui porque o deploy só ocorre na main exata
            "token.actions.githubusercontent.com:sub" = "repo:empresa/data-platform:ref:refs/heads/main"
          }
        }
      }
    ]
  })
}

# Permissões da Role de deploy
resource "aws_iam_role_policy" "github_deploy_policy" {
  name = "data-pipeline-deploy"
  role = aws_iam_role.github_deploy.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # ECR: push de imagens Docker do pipeline
      {
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:PutImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload"
        ]
        Resource = "*"
      },
      # S3: sync de artefatos e dados processados
      {
        Effect   = "Allow"
        Action   = ["s3:PutObject", "s3:GetObject", "s3:ListBucket"]
        Resource = [
          "arn:aws:s3:::empresa-artifacts/*",
          "arn:aws:s3:::empresa-artifacts"
        ]
      },
      # Glue: disparar jobs de ETL
      {
        Effect = "Allow"
        Action = [
          "glue:StartJobRun",
          "glue:GetJobRun",
          "glue:GetJob"
        ]
        Resource = "arn:aws:glue:sa-east-1:*:job/etl-*"
      }
    ]
  })
}
# ─── Role separada para PRs (somente leitura) ──────────────────────

resource "aws_iam_role" "github_readonly" {
  name = "github-oidc-data-readonly"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
        }
        StringLike = {
          # Mantemos StringLike aqui com o curinga (*) para validar PRs de qualquer branch
          "token.actions.githubusercontent.com:sub" = "repo:empresa/data-platform:*"
        }
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "github_readonly" {
  role       = aws_iam_role.github_readonly.name
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

## GitHub Actions - Workflow de Pipeline de Dados

# .github/workflows/data-pipeline.yml

name: Deploy Data Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  id-token: write   # OBRIGATÓRIO: permite gerar o JWT OIDC
  contents: read

jobs:
  # ── Validação em PRs (role readonly) ──────────────────────────────
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Assume role de validação (readonly)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-oidc-data-readonly
          aws-region: sa-east-1
          role-session-name: pr-validation-${{ github.run_id }}

      - name: Valida estrutura do Data Lake
        run: |
          aws s3 ls s3://empresa-datalake-prod/raw/ --recursive | head -20
          aws glue get-job --job-name etl-vendas

  # ── Deploy em push para main (role com permissões reais) ───────────
  deploy:
    runs-on: ubuntu-latest
    needs: validate
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - uses: actions/checkout@v4

      - name: Assume role de deploy
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-oidc-data-deploy
          aws-region: sa-east-1
          role-session-name: deploy-${{ github.run_id }}-${{ github.sha }}

      - name: Login no ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build e push da imagem do pipeline
        env:
          ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/data-pipeline:$IMAGE_TAG .
          docker push $ECR_REGISTRY/data-pipeline:$IMAGE_TAG
          docker tag $ECR_REGISTRY/data-pipeline:$IMAGE_TAG \
                     $ECR_REGISTRY/data-pipeline:latest
          docker push $ECR_REGISTRY/data-pipeline:latest

      - name: Sincroniza scripts Glue com S3
        run: |
          aws s3 sync ./glue_jobs/ s3://empresa-artifacts/glue-scripts/ \
            --exclude "*.pyc" \
            --delete

      - name: Dispara job Glue de ETL
        run: |
          JOB_RUN_ID=$(aws glue start-job-run \
            --job-name etl-vendas \
            --arguments '{"--env":"prod","--date":"'"$(date +%Y-%m-%d)"'"}' \
            --query 'JobRunId' --output text)

          echo "Job iniciado: $JOB_RUN_ID"

          # Aguarda conclusão
          while true; do
            STATUS=$(aws glue get-job-run \
              --job-name etl-vendas \
              --run-id $JOB_RUN_ID \
              --query 'JobRun.JobRunState' --output text)

            echo "Status: $STATUS"

            if [[ "$STATUS" == "SUCCEEDED" ]]; then
              echo "✅ ETL concluído com sucesso"
              break
            elif [[ "$STATUS" == "FAILED" || "$STATUS" == "ERROR" ]]; then
              echo "❌ ETL falhou"
              exit 1
            fi
            sleep 30
          done

## Outros provedores OIDC - GitLab, CircleCI

# GitLab CI
resource "aws_iam_openid_connect_provider" "gitlab" {
  url = "https://gitlab.com"
  # Padronizando o audience para STS, seguindo as melhores práticas atuais do GitLab id_tokens
  client_id_list  = ["sts.amazonaws.com"] 
  thumbprint_list = ["..."]
}

resource "aws_iam_role" "gitlab_deploy" {
  name = "gitlab-oidc-deploy"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Federated = aws_iam_openid_connect_provider.gitlab.arn }
      Action    = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          # sub do GitLab: project_path:grupo/repo:ref_type:branch:ref:main
          "gitlab.com:sub" = "project_path:empresa/data-platform:ref_type:branch:ref:main"
        }
      }
    }]
  })
}

## Python - Usando STS diretamente (cross-account)

import boto3
from datetime import datetime

def assumir_role_dados_prod(account_id: str, role_name: str) -> dict:
    """
    Assume uma role em outra conta AWS para acessar dados de produção.
    Útil em pipelines que rodam na conta-dev e precisam ler da conta-prod.
    """
    sts = boto3.client('sts')

    role_arn = f"arn:aws:iam::{account_id}:role/{role_name}"
    session_name = f"pipeline-{datetime.now().strftime('%Y%m%d%H%M%S')}"

    response = sts.assume_role(
        RoleArn=role_arn,
        RoleSessionName=session_name,
        DurationSeconds=3600,          # 1 hora (padrão)
        # Tags que ficam registradas no CloudTrail
        Tags=[
            {'Key': 'Pipeline', 'Value': 'etl-vendas'},
            {'Key': 'Environment', 'Value': 'prod'}
        ]
    )

    creds = response['Credentials']
    print(f"✅ Role assumida. Expira em: {creds['Expiration']}")
    return creds

def criar_cliente_cross_account(service: str, account_id: str, role_name: str):
    """
    Cria um cliente boto3 autenticado na conta de destino.
    Padrão usado em pipelines multi-conta.
    """
    creds = assumir_role_dados_prod(account_id, role_name)

    return boto3.client(
        service,
        aws_access_key_id     = creds['AccessKeyId'],
        aws_secret_access_key = creds['SecretAccessKey'],
        aws_session_token     = creds['SessionToken'],
        region_name           = 'sa-east-1'
    )

# Uso em pipeline cross-account
s3_prod = criar_cliente_cross_account(
    service     = 's3',
    account_id  = '111122223333',   # conta de produção
    role_name   = 'DataReadRole'
)

# Agora lê dados de produção com credenciais temporárias
response = s3_prod.list_objects_v2(
    Bucket='empresa-datalake-prod',
    Prefix='refined/vendas/'
)

arquivos = [obj['Key'] for obj in response.get('Contents', [])]
print(f"Arquivos encontrados: {len(arquivos)}")

6. COMO CAI EM CERTIFICAÇÕES AWS

Tabela de conceitos-chave

Pegadinhas clássicas:

  1. id-token: write é obrigatório no GitHub Actions
# SEM isso, o workflow não consegue gerar o JWT OIDC
permissions:
  id-token: write   ← OBRIGATÓRIO
  contents: read
  1. O problema do “confused deputy”
Cenário: Empresa A configura Role que confia em Empresa B (parceiro).
Problema: Empresa B pode usar a Role para acessar dados de Empresa C
         (outra cliente da B que também usa AWS).

Solução: ExternalId - um segredo combinado entre A e B.
         A Role só aceita assume se B enviar o ExternalId correto.

resource "aws_iam_role" "parceiro" {
  assume_role_policy = jsonencode({
    ...
    Condition = {
      StringEquals = {
        "sts:ExternalId" = "segredo-combinado-123"
      }
    }
  })
}

Nota de Arquitetura: O ExternalId resolve o Confused Deputy no cenário tradicional entre contas AWS (sts:AssumeRole). Quando usamos OIDC (sts:AssumeRoleWithWebIdentity), quem faz exatamente esse papel de nos proteger contra o Confused Deputy são as claims sub (identidade precisa do repositório) e aud (audiência), que amarramos na Condition da Trust Policy.

  1. Duração máxima de sessão configurável na Role
Padrão: 1 hora
Mínimo: 15 minutos
Máximo configurável: até 12 horas (define na Role, não na chamada)

Na prova: "pipeline longo precisa de mais de 1h de credenciais"
→ Aumente o MaxSessionDuration da Role (até 12h)
→ Ou redesenhe para renovar credenciais no meio do processo
  1. Sub-claim do OIDC é o que protege contra abuso
sub = "repo:empresa/app:ref:refs/heads/main"

Se você colocar StringLike com "*" no sub,
qualquer repositório poderia assumir sua role!

Sempre seja específico:
  ✅ "repo:minha-empresa/meu-repo:ref:refs/heads/main"
  ❌ "repo:*"
  ❌ Sem Condition nenhuma
  1. STS credentials têm 3 partes - as 3 são obrigatórias
# ERRADO: usar só AccessKey e SecretKey de credencial temporária
boto3.client('s3',
    aws_access_key_id='ASIA...',
    aws_secret_access_key='...'
    # ← faltou SessionToken → vai dar InvalidClientTokenId
)

# CERTO:
boto3.client('s3',
    aws_access_key_id='ASIA...',
    aws_secret_access_key='...',
    aws_session_token='...'   ← OBRIGATÓRIO para creds temporárias
)

7. MAPA MENTAL

STS + OIDC
├── STS (Security Token Service)
│   ├── Emite credenciais temporárias (AccessKey + Secret + Token + Expiration)
│   ├── AssumeRole             → delegação interna ou cross-account
│   ├── AssumeRoleWithWebIdentity → OIDC/JWT (GitHub, mobile)
│   ├── AssumeRoleWithSAML     → SSO corporativo (Okta, Azure AD)
│   └── GetSessionToken        → humano com MFA
│
├── OIDC (OpenID Connect)
│   ├── Protocolo padrão de identidade baseado em JWT
│   ├── Provedores: GitHub, GitLab, CircleCI, Google, etc.
│   ├── IAM OIDC Provider = confiança registrada na conta AWS
│   └── Sub-claim = identidade precisa (repo + branch + org)
│
├── TRUST POLICY (quem pode assumir a role)
│   ├── Principal: Federated (OIDC) ou AWS (conta/role)
│   ├── Action: sts:AssumeRoleWithWebIdentity
│   └── Condition: StringEquals/StringLike nas claims do JWT
│
└── BOAS PRÁTICAS
    ├── Nunca chaves estáticas em CI/CD → sempre OIDC
    ├── Roles separadas por contexto (deploy ≠ readonly ≠ infra)
    ├── ExternalId para parceiros externos
    ├── Condition no sub-claim sempre específico
    └── Session name descritivo para auditoria no CloudTrail

8. FLASHCARDS

Por que usar OIDC em vez de chaves IAM no GitHub Actions? Chaves IAM são permanentes e podem vazar. OIDC gera credenciais temporárias por execução - sem segredo armazenado, sem rotação manual, sem risco de vazamento em logs.

O que é o sub-claim no JWT OIDC do GitHub? É a identidade precisa do executor: repo:org/repo:ref:refs/heads/main. É o que a Condition da trust policy valida para garantir que só o repositório certo pode assumir a role.

Qual a diferença entre AssumeRole e AssumeRoleWithWebIdentity? *AssumeRole usa credenciais AWS para delegar acesso (same account ou cross-account). AssumeRoleWithWebIdentity usa um JWT externo (OIDC) - não precisa de credenciais AWS para iniciar o fluxo.*

O que é ExternalId e quando usar?

  • É um segredo combinado entre você e um parceiro externo para evitar o “confused deputy attack”. Use sempre que uma empresa terceira vai assumir uma role na sua conta.*

9. PENSE COMO ENGENHEIRO

Cenários reais de decisão

Problema 1 - “Troubleshooting: GitHub Actions com erro InvalidIdentityToken .”

Causas mais comuns:
1. permissions.id-token: write não está no workflow
2. O sub-claim na trust policy não bate com o repositório real
   (verifique: org/repo, branch, se é PR ou push)
3. O OIDC Provider não foi criado na conta correta
4. Audience errado (deve ser "sts.amazonaws.com")

Debug:
  # No workflow, adicione um step para ver o token decodificado
  - name: Debug OIDC token
    run: |
      curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
           "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | \
      python3 -c "import sys,json,base64; t=json.load(sys.stdin)['value'];
      h,p,s=t.split('.'); print(json.dumps(json.loads(
      base64.b64decode(p+'==').decode()), indent=2))"

Problema 2 - Design: “Como dar acesso ao Glue em conta de produção a partir da conta de CI/CD?”

Problema 3 - Segurança: “Como garantir que deploy só roda de commits aprovados?”

# Adiciona proteção de environment no GitHub
deploy:
  environment: production        # ← requer aprovação manual
  if: github.ref == 'refs/heads/main'
  steps:
    - name: Assume role de deploy
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ secrets.DEPLOY_ROLE_ARN }}
        # O sub-claim já garante que é branch main
        # O environment garante aprovação humana

## Trade-offs importantes:

10. EXERCÍCIOS

## Quiz Rápido

1. Um workflow do GitHub Actions falha com Not authorized to perform sts:AssumeRoleWithWebIdentity. Qual é a causa mais provável?

  • a) A role não tem permissão para acessar S3
  • b) A trust policy da role não tem o sub-claim do repositório correto ✅
  • c) O STS está fora do ar na região
  • d) Falta a policy AmazonSTSFullAccess

2. Qual a diferença entre AssumeRole e AssumeRoleWithWebIdentity?

  • a) Nenhuma - são sinônimos
  • b) AssumeRole usa credenciais AWS como entrada; AssumeRoleWithWebIdentity usa um JWT externo ✅
  • c) AssumeRoleWithWebIdentity só funciona com Google
  • d) AssumeRole não funciona cross-account

3. Um parceiro externo vai assumir uma role na sua conta. Como proteger contra confused deputy attack?

  • a) Criar um IAM User separado para o parceiro
  • b) Usar ExternalId na Condition da trust policy ✅
  • c) Ativar MFA na role
  • d) Usar SCPs

## Lab Prático

Lab - Pipeline de dados com OIDC end-to-end:

1. Crie um repositório no GitHub com um workflow simples
2. No Terraform, configure o OIDC Provider para GitHub
3. Crie uma Role com trust policy específica pro seu repo/main
4. Dê permissão: s3:ListBucket no seu bucket de teste
5. No workflow, use aws-actions/configure-aws-credentials
6. Step de teste: aws s3 ls s3://seu-bucket → deve funcionar ✅
7. Troque o branch na Condition por "refs/heads/outra-branch"
8. Faça push em main → deve falhar ✅ (branch não bate)
9. Adicione um segundo job com role readonly para PRs
10. Abra um PR → valide que só o job readonly roda ✅

Próximo Passo (Parte 5)

AWS KMS - criptografia de ponta a ponta (a última camada de proteção dos seus dados) - Em breve


메타데이터
post_id
e31fb3e10d26
slug
aws-para-engenheiros-de-dados-parte-4-aws-sts-oidc-autenticação-federada-e-ci-cd-e31fb3e10d26
url
https://medium.com/@rafa-trindade/aws-para-engenheiros-de-dados-parte-4-aws-sts-oidc-autentica%C3%A7%C3%A3o-federada-e-ci-cd-e31fb3e10d26
canonical_url
https://medium.com/@rafa-trindade/aws-para-engenheiros-de-dados-parte-4-aws-sts-oidc-autentica%C3%A7%C3%A3o-federada-e-ci-cd-e31fb3e10d26
author_url
https://medium.com/@rafa-trindade
status
ok
fetched_at
2026-08-02 11:33:19