Variáveis de Ambiente e Secrets em Pipelines

[290] Variáveis de Ambiente e Secrets em Pipelines

A distinção entre configuração e segredo, e o que fazer com cada um: os três escopos de secret do GitHub, o mascaramento nos logs e onde ele falha, o GITHUB_TOKEN com permissions ajustadas por workflow, a integração com Vault e AWS Secrets Manager, o .env que nunca é versionado, e a rotação como rotina.
DevOps

18 min de leitura

Em abril de 2022, a empresa de segurança GitGuardian publicou um relatório revelando que mais de seis milhões de secrets foram expostos em repositórios públicos do GitHub apenas naquele ano — um aumento de 25% em relação ao ano anterior. Chaves de API da AWS, senhas de banco de dados, tokens de autenticação, credenciais de serviços de pagamento. A maioria deles ainda estava ativa no momento da descoberta.

A causa mais comum não é negligência deliberada. É a falta de um processo claro para gerenciar informações sensíveis em pipelines de CI/CD. Um desenvolvedor adiciona uma chave diretamente no arquivo de workflow para "testar rapidamente", o teste funciona, o commit vai para o repositório e a chave fica exposta para sempre no histórico — mesmo que seja removida em um commit posterior.

Entender como gerenciar secrets corretamente não é opcional. É uma competência fundamental de qualquer profissional de DevOps.

A Distinção entre Variáveis de Ambiente e Secrets

Antes de entrar nas ferramentas, é importante estabelecer uma distinção conceitual que muitas equipes ignoram.

Variáveis de ambiente são configurações que variam entre ambientes mas não são sensíveis. O nome do ambiente (NODE_ENV=production), a porta da aplicação (PORT=3000), a URL base de uma API pública, o nível de log (LOG_LEVEL=info) — todas essas informações podem viver em arquivos de configuração versionados sem risco.

Secrets são informações sensíveis cuja exposição representa um risco de segurança. Senhas de banco de dados, chaves de API, tokens OAuth, certificados, chaves SSH, chaves de criptografia — essas informações nunca devem aparecer em código, em logs ou em qualquer lugar que não seja um sistema de gerenciamento de secrets com controle de acesso e auditoria.

A confusão entre os dois conceitos é a origem de muitos vazamentos. Não basta proteger as senhas se as URLs de conexão com o banco contêm a senha embutida. Não basta não commitar o .env se o pipeline imprime variáveis de ambiente nos logs de debug.

Secrets no GitHub Actions

O GitHub oferece três níveis de secrets, cada um com escopo e visibilidade diferentes.

Repository secrets — disponíveis apenas no repositório onde foram criados. Adequados para projetos individuais ou segredos específicos de um repositório.

Environment secrets — vinculados a um ambiente específico (staging, production). Só ficam disponíveis quando o job está rodando naquele ambiente. Permitem ter a mesma chave com valores diferentes por ambiente.

Organization secrets — disponíveis para múltiplos repositórios dentro de uma organização. Adequados para segredos compartilhados como tokens de acesso a registros de container ou chaves de integração com ferramentas externas.

Criando um secret via GitHub CLI:

# Secret de repositório
gh secret set DATABASE_URL --body "postgresql://user:senha@host:5432/db"

# Secret de ambiente específico
gh secret set DATABASE_URL \
  --env production \
  --body "postgresql://user:senha_prod@host-prod:5432/db"

# A partir de um arquivo
gh secret set CHAVE_PRIVADA_SSH < ~/.ssh/id_ed25519

# Listando secrets existentes (apenas os nomes — valores nunca são exibidos)
gh secret list
gh secret list --env production

Usando Secrets em Workflows

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      # Forma 1: como variável de ambiente do step
      - name: Conecta ao banco
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: npm run db:migrate

      # Forma 2: como input de uma action
      - name: Login no registro Docker
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Forma 3: escrevendo em arquivo temporário
      - name: Configura chave SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.CHAVE_SSH_PRIVADA }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H ${{ secrets.SERVIDOR_HOST }} >> ~/.ssh/known_hosts

O GitHub Actions mascara automaticamente os valores de secrets nos logs. Se um secret tiver o valor minha-senha-secreta e esse valor aparecer em qualquer linha de log, ele será substituído por ***. No entanto, esse mascaramento tem limitações — valores muito curtos ou que aparecem como parte de strings maiores podem não ser mascarados corretamente.

O GITHUB_TOKEN: O Secret Automático

Todo workflow do GitHub Actions tem acesso automático a um token especial chamado GITHUB_TOKEN. Esse token é gerado pelo GitHub no início de cada execução de workflow e expirado ao final. Ele tem permissões para interagir com o repositório onde o workflow está rodando.

As permissões padrão do GITHUB_TOKEN podem ser configuradas no nível do repositório ou sobrescritas por workflow:

# Permissões mínimas para o workflow inteiro
permissions:
  contents: read
  packages: write
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-latest
    # Permissões específicas para este job
    permissions:
      contents: read
      packages: write

    steps:
      - name: Login no GHCR usando o token automático
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

O princípio de menor privilégio se aplica diretamente aqui: cada workflow deve ter apenas as permissões que realmente precisa. Um workflow que apenas executa testes não precisa de permissão para escrever no repositório.

Variáveis de Ambiente por Ambiente

O GitHub Actions permite definir variáveis de ambiente em diferentes níveis de escopo, do mais amplo ao mais específico:

# Nível do workflow — disponível em todos os jobs
env:
  NODE_VERSION: '20.x'
  REGISTRY: ghcr.io

jobs:
  build:
    runs-on: ubuntu-latest
    # Nível do job — disponível em todos os steps deste job
    env:
      NODE_ENV: production
      APP_PORT: 3000

    steps:
      - name: Build
        # Nível do step — disponível apenas neste step
        env:
          BUILD_TARGET: dist
        run: npm run build -- --outDir $BUILD_TARGET

Para variáveis que mudam entre ambientes, o recurso de environment variables do GitHub Environments é o mais adequado:

No repositório, em Settings → Environments, cria-se o ambiente e adicionam-se variáveis não-sensíveis diretamente ali. Elas ficam disponíveis nos jobs que referenciam aquele ambiente:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # carrega variáveis e secrets deste ambiente

    steps:
      - name: Deploy
        env:
          # Variável definida no ambiente 'production' do GitHub
          API_URL: ${{ vars.API_URL }}
          # Secret definido no ambiente 'production'
          API_KEY: ${{ secrets.API_KEY }}
        run: ./scripts/deploy.sh

Gerenciadores Externos de Secrets

Para organizações com requisitos mais avançados de segurança ou com infraestrutura multi-cloud, os secrets do GitHub podem não ser suficientes. Os gerenciadores externos oferecem funcionalidades como rotação automática de credenciais, auditoria detalhada e integração com sistemas de identidade corporativos.

HashiCorp Vault é a solução open source mais consolidada. Integra com GitHub Actions via action oficial:

- name: Importa secrets do Vault
  uses: hashicorp/vault-action@v3
  with:
    url: https://vault.minha-empresa.com
    token: ${{ secrets.VAULT_TOKEN }}
    secrets: |
      secret/data/producao/banco password | DATABASE_PASSWORD;
      secret/data/producao/api key | API_KEY;
      secret/data/producao/redis url | REDIS_URL

AWS Secrets Manager é a solução gerenciada da AWS. Cada secret tem controle de acesso via IAM e suporte nativo a rotação automática:

- name: Configura credenciais AWS
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: us-east-1

- name: Importa secrets do AWS Secrets Manager
  run: |
    SECRET=$(aws secretsmanager get-secret-value \
      --secret-id producao/minha-api \
      --query SecretString \
      --output text)

    # Extrai valores individuais do JSON
    DATABASE_URL=$(echo $SECRET | jq -r '.database_url')
    API_KEY=$(echo $SECRET | jq -r '.api_key')

    # Exporta para os steps seguintes de forma segura
    echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV
    echo "API_KEY=$API_KEY" >> $GITHUB_ENV
    echo "::add-mask::$DATABASE_URL"
    echo "::add-mask::$API_KEY"

O comando ::add-mask:: instrui o GitHub Actions a mascarar aquele valor específico nos logs — essencial quando o valor vem de uma fonte externa e o GitHub não sabe automaticamente que deve ser mascarado.

O Arquivo .env em Desenvolvimento

Em desenvolvimento local, o arquivo .env é a forma mais prática de gerenciar configurações. A regra de ouro é simples: o .env nunca vai para o repositório.

Estrutura recomendada para projetos com múltiplos ambientes:

.env                  # Não versionado — valores locais do desenvolvedor
.env.example          # Versionado — template com todas as chaves, sem valores
.env.test             # Pode ser versionado — apenas valores de teste, sem dados reais
.env.production       # Nunca versionado — gerenciado pelo pipeline

O .env.example serve como documentação viva do que o sistema precisa para funcionar:

# .env.example
# Banco de dados
DATABASE_URL=postgresql://usuario:senha@localhost:5432/nome_do_banco

# Cache
REDIS_URL=redis://localhost:6379

# Autenticação
JWT_SECRET=gere-um-valor-aleatorio-aqui
JWT_EXPIRES_IN=7d

# Serviço de email
SMTP_HOST=smtp.exemplo.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=

# Integrações externas
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Configurações da aplicação
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug

A biblioteca dotenv carrega esse arquivo automaticamente em desenvolvimento:

// src/config/env.js
require('dotenv').config();

const config = {
  database: {
    url: process.env.DATABASE_URL,
  },
  redis: {
    url: process.env.REDIS_URL,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '7d',
  },
  server: {
    port: parseInt(process.env.PORT || '3000'),
    env: process.env.NODE_ENV || 'development',
  },
};

// Valida que todas as variáveis obrigatórias estão presentes
const required = ['DATABASE_URL', 'REDIS_URL', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);

if (missing.length > 0) {
  throw new Error(
    `Variáveis de ambiente obrigatórias não definidas: ${missing.join(', ')}`
  );
}

module.exports = config;

Essa validação na inicialização da aplicação evita que a aplicação suba silenciosamente com configurações incompletas e falhe de forma inesperada em runtime.

Auditoria e Rotação de Secrets

Secrets que nunca são rotacionados são um risco crescente. Um secret que foi exposto acidentalmente há seis meses pode ainda estar sendo explorado silenciosamente.

Boas práticas de ciclo de vida de secrets:

Rotação periódica — secrets de alto valor devem ser rotacionados regularmente, mesmo que não haja suspeita de comprometimento. O AWS Secrets Manager e o HashiCorp Vault suportam rotação automática.

Revogação imediata — quando um desenvolvedor sai da empresa ou quando um secret é acidentalmente exposto, a revogação deve ser imediata. Processos que dependem de secrets pessoais — chaves SSH de desenvolvedores individuais, tokens gerados por contas pessoais — são um risco operacional.

Auditoria de uso — saber quem acessou qual secret e quando é fundamental para investigação de incidentes. Gerenciadores como Vault e AWS Secrets Manager mantêm logs de acesso detalhados.

Scanning no código — ferramentas como o GitGuardian e o git-secrets verificam automaticamente se secrets foram commitados acidentalmente:

# Instala o git-secrets
brew install git-secrets   # macOS
# ou compila da fonte no Linux

# Configura para verificar padrões da AWS
git secrets --register-aws

# Adiciona como hook pre-commit
git secrets --install

# Verifica o repositório inteiro em busca de secrets já commitados
git secrets --scan-history

Integrado ao pipeline de CI:

- name: Verifica secrets no código
  run: |
    pip install detect-secrets
    detect-secrets scan --baseline .secrets.baseline
    detect-secrets audit .secrets.baseline

Um Workflow Completo com Gerenciamento Seguro de Secrets

Reunindo todas as práticas em um workflow realista:

name: Deploy Seguro

on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write
  id-token: write  # necessário para OIDC com AWS

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      # Autenticação sem chaves estáticas via OIDC
      - name: Configura credenciais AWS via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
          aws-region: us-east-1

      # Importa secrets do gerenciador externo
      - name: Importa secrets de produção
        run: |
          SECRETS=$(aws secretsmanager get-secret-value \
            --secret-id producao/minha-api \
            --query SecretString --output text)

          DB_URL=$(echo $SECRETS | jq -r '.database_url')
          REDIS_URL=$(echo $SECRETS | jq -r '.redis_url')

          # Mascara os valores nos logs
          echo "::add-mask::$DB_URL"
          echo "::add-mask::$REDIS_URL"

          # Exporta para os steps seguintes
          echo "DATABASE_URL=$DB_URL" >> $GITHUB_ENV
          echo "REDIS_URL=$REDIS_URL" >> $GITHUB_ENV

      # Build da imagem com o secret disponível apenas durante o build
      - name: Constrói imagem Docker
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          secrets: |
            database_url=${{ env.DATABASE_URL }}

      # Deploy usando a imagem publicada
      - name: Deploy no ECS
        run: |
          aws ecs update-service \
            --cluster producao \
            --service minha-api \
            --force-new-deployment

      # Limpa variáveis sensíveis do ambiente
      - name: Limpa variáveis sensíveis
        if: always()
        run: |
          echo "DATABASE_URL=" >> $GITHUB_ENV
          echo "REDIS_URL=" >> $GITHUB_ENV

O Que Vem a Seguir

No próximo artigo será abordado o deploy automático para servidores com GitHub Actions — coberturas completas de estratégias de deploy, zero-downtime deployment, rollback automatizado e como estruturar o pipeline para que a promoção entre ambientes seja segura e auditável.

Referências para Aprofundamento

Segurança e boas práticas

Ferramentas

OIDC e autenticação sem chaves estáticas

Exercícios

Exercício 1

Qual é a diferença entre uma variável de ambiente e um secret? Classifique cada item a seguir e justifique: NODE_ENV=production, LOG_LEVEL=info, postgresql://user:senha@host:5432/db, PORT=3000.

Ver resposta

✓ Resposta: Variáveis de ambiente são configurações que mudam entre ambientes mas não são sensíveis — podem viver em arquivos versionados. Secrets são informações cuja exposição representa risco de segurança e só devem existir em um sistema com controle de acesso e auditoria.

NODE_ENV, LOG_LEVEL e PORT são variáveis de ambiente comuns. Já a connection string é um secret, e é justamente a pegadinha do exercício: ela parece uma URL de configuração, mas tem a senha embutida (user:senha@). É o caso que o artigo destaca — não basta proteger o campo "senha" se a URL de conexão carrega a senha dentro dela. A segunda armadilha citada é a mesma ideia por outro caminho: não basta manter o .env fora do repositório se o pipeline imprime as variáveis de ambiente nos logs de debug.

Exercício 2

O GitHub oferece três níveis de secrets. Qual deles você usaria para ter DATABASE_URL com valores diferentes em staging e em produção, e o que precisa existir no job para que esse valor seja carregado?

Ver resposta

✓ Resposta: Environment secrets. Os três níveis são: repository secrets (só no repositório onde foram criados), environment secrets (vinculados a um ambiente, disponíveis apenas quando o job roda nele) e organization secrets (compartilhados entre repositórios da organização, úteis para tokens de registro de container).

Só os environment secrets permitem a mesma chave com valores distintos por ambiente. Para que funcione, o job precisa declarar environment: production — é essa linha que carrega as variáveis e secrets daquele ambiente. Sem ela, ${{ secrets.DATABASE_URL }} não enxerga o valor do ambiente. Na CLI, a criação é gh secret set DATABASE_URL --env production --body "...".

Exercício 3

O que é o GITHUB_TOKEN, quando ele é criado e quando deixa de valer? Um workflow que apenas roda testes deveria declarar permissions: contents: write? Justifique pelo princípio aplicável.

Ver resposta

✓ Resposta: É um token gerado automaticamente pelo GitHub no início de cada execução de workflow e expirado ao final dela, com permissões para interagir com o repositório onde o workflow roda. Como é efêmero e não precisa ser criado nem rotacionado por ninguém, ele deve ser preferido a um Personal Access Token sempre que a tarefa se limitar ao próprio repositório — no artigo, por exemplo, o login no ghcr.io usa ${{ secrets.GITHUB_TOKEN }}.

Não: um workflow que só roda testes não deve pedir contents: write. Pelo princípio de menor privilégio, cada workflow recebe apenas as permissões de que realmente precisa — nesse caso, contents: read basta. Conceder escrita amplia sem necessidade o estrago possível caso o workflow execute código de terceiros (uma action comprometida ou um PR malicioso).

Exercício 4

O GitHub Actions mascara secrets nos logs automaticamente. Então por que o exemplo que busca credenciais no AWS Secrets Manager precisa chamar echo "::add-mask::$DATABASE_URL" explicitamente?

Ver resposta

✓ Resposta: Porque o mascaramento automático só cobre os valores que o GitHub já conhece — os cadastrados como secrets do repositório, do ambiente ou da organização. O valor vindo do AWS Secrets Manager é obtido em runtime, dentro do step: para o GitHub ele é apenas texto qualquer que apareceu na saída de um comando, e seria impresso normalmente. O ::add-mask:: registra aquele valor específico na lista de mascaramento a partir dali, fazendo com que qualquer ocorrência posterior vire ***.

Vale notar o alerta do artigo sobre o próprio mascaramento automático: ele tem limitações — valores muito curtos, ou que aparecem como parte de strings maiores, podem não ser mascarados corretamente. Mascarar não substitui não vazar.

Exercício 5

No módulo src/config/env.js, qual é o efeito prático do bloco abaixo, e por que ele é melhor do que deixar a aplicação subir e falhar depois? E por que o .env.example é versionado enquanto o .env não é?

const required = ['DATABASE_URL', 'REDIS_URL', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);
Ver resposta

✓ Resposta: Ele faz a aplicação falhar imediatamente na inicialização, com uma mensagem que nomeia exatamente quais variáveis obrigatórias faltam. Sem essa checagem, o processo sobe normalmente e só quebra quando a primeira requisição tocar o banco ou tentar assinar um JWT — possivelmente em produção, com erro genérico e longe da causa. Falhar no boot transforma um erro de configuração difícil de diagnosticar em uma mensagem direta, antes de o serviço receber tráfego.

Quanto aos arquivos: o .env contém os valores reais do desenvolvedor e nunca vai para o repositório. O .env.example traz todas as chaves sem os valores, funcionando como documentação viva do que o sistema precisa para rodar — é ele que permite a alguém clonar o projeto e saber o que configurar. O artigo ainda separa .env.test (pode ser versionado, só valores de teste) e .env.production (nunca versionado, gerenciado pelo pipeline).

Comentários

Mais em DevOps

Gerando e Revisando Pipelines com IA
Gerando e Revisando Pipelines com IA

Prompting aplicado a infraestrutura: os quatro elementos de contexto que…

Introdução ao Terraform: Infraestrutura que Você Pode Versionar
Introdução ao Terraform: Infraestrutura que Você Pode Versionar

O problema dos snowflake servers e a infraestrutura que se pode versionar: o…

Projeto Capstone: Arquitetura do Sistema Completo
Projeto Capstone: Arquitetura do Sistema Completo

A arquitetura do projeto que integra toda a série em um sistema de produção…