Git secrets: prevenindo commit de credenciais acidentalmente

1. O Problema: Credenciais no Histórico do Git

1.1. Cenários comuns de vazamento

O vazamento de credenciais em repositórios Git é um dos incidentes de segurança mais frequentes e perigosos no desenvolvimento de software. Os cenários típicos incluem:

  • Um desenvolvedor faz commit de um arquivo .env contendo senhas de banco de dados
  • Chaves SSH privadas (id_rsa, *.pem) são incluídas acidentalmente em um commit
  • Tokens de API da AWS, GitHub ou Stripe aparecem em arquivos de configuração
  • Strings de conexão com bancos de produção são hardcoded no código-fonte

1.2. Consequências irreversíveis mesmo após remover o commit

Muitos desenvolvedores acreditam que, ao remover um commit com git reset ou git revert, o problema está resolvido. Isso é uma ilusão perigosa. O Git mantém todo o histórico de commits no repositório local e remoto. Mesmo após um git push --force, os commits antigos permanecem acessíveis via git reflog por pelo menos 90 dias no GitHub e GitLab.

Uma credencial commitada acidentalmente pode:
- Ser descoberta por ferramentas de varredura automática (como GitHub Secret Scanning)
- Permitir acesso não autorizado a serviços críticos
- Gerar custos financeiros significativos (ex: uso não autorizado de serviços cloud)
- Expor dados de clientes e violar regulamentações como LGPD ou GDPR

1.3. Como o Git trata arquivos rastreados vs. não rastreados

A diferença entre arquivos rastreados (tracked) e não rastreados (untracked) é crucial para entender a prevenção de vazamentos:

  • Arquivos não rastreados: arquivos que nunca foram adicionados ao staging. O .gitignore funciona perfeitamente aqui.
  • Arquivos rastreados: arquivos que já estão no índice do Git. Uma vez rastreados, o .gitignore não tem efeito sobre eles.
# Verificando se um arquivo está rastreado
git ls-files --error-unmatch config/credentials.json
# Se retornar o nome do arquivo, ele está rastreado
# Se retornar erro, não está rastreado

2. .gitignore: Primeira Linha de Defesa

2.1. Padrões globais e locais para excluir arquivos sensíveis

O .gitignore é a ferramenta mais básica e essencial. Crie um arquivo .gitignore na raiz do repositório com padrões específicos:

# Arquivos de ambiente
.env
.env.local
.env.production

# Chaves e certificados
*.pem
*.key
*.cert
id_rsa
id_rsa.pub

# Arquivos de configuração com credenciais
credentials.json
config/database.yml
secrets.yml

# Diretórios comuns de configuração
.aws/
.gcp/
.ssh/

2.2. Cuidados com .gitignore após o primeiro commit

Se um arquivo sensível já foi commitado, adicioná-lo ao .gitignore não resolve o problema. O Git continuará rastreando as alterações desse arquivo. Para corrigir:

# Remover do índice sem deletar o arquivo local
git rm --cached config/credentials.json

# Agora sim, o .gitignore funcionará para esse arquivo
echo "config/credentials.json" >> .gitignore
git add .gitignore
git commit -m "Remove credentials.json do rastreamento"

2.3. Exemplos práticos

# .gitignore completo para projetos Node.js com credenciais
node_modules/
dist/
.env
.env.*
*.log
npm-debug.log*
.DS_Store
coverage/
.nyc_output/
*.pem
*.key
secrets/
config/production.json

3. Git Hooks Client-Side para Bloqueio Automático

3.1. Pre-commit hook: escaneando arquivos antes do commit

Git hooks são scripts que executam automaticamente em eventos específicos. O pre-commit hook roda antes do commit ser criado, permitindo bloquear commits que contenham padrões suspeitos.

3.2. Implementação de um hook simples com grep e exit 1

Crie o arquivo .git/hooks/pre-commit:

#!/bin/sh

# Padrões de credenciais a serem verificados
PATTERNS="password=|senha=|api_key=|secret=|token=|aws_secret|-----BEGIN RSA PRIVATE KEY-----"

echo "🔍 Verificando credenciais nos arquivos modificados..."

# Lista arquivos staged (excluindo deletados)
FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [ -z "$FILES" ]; then
    exit 0
fi

# Verifica cada arquivo
for FILE in $FILES; do
    if [ -f "$FILE" ]; then
        if grep -qE "$PATTERNS" "$FILE"; then
            echo "❌ ERRO: Credencial detectada em $FILE"
            echo "   Remova a credencial antes de commitar."
            exit 1
        fi
    fi
done

echo "✅ Nenhuma credencial encontrada. Commit permitido."
exit 0

Torne o hook executável:

chmod +x .git/hooks/pre-commit

3.3. Compartilhando hooks com a equipe via core.hooksPath

Para compartilhar hooks com toda a equipe, armazene-os em um diretório versionado:

# Estrutura do repositório
meu-projeto/
├── .githooks/
│   └── pre-commit
├── .gitignore
└── src/

# Configure cada desenvolvedor para usar esse diretório
git config core.hooksPath .githooks

Adicione essa configuração ao README.md ou a um script de setup automático.

4. Git Secrets: Ferramenta Open Source da AWS Labs

4.1. Instalação e configuração básica

Git Secrets é uma ferramenta desenvolvida pela AWS Labs que automatiza a detecção de credenciais:

# Instalação no macOS (Homebrew)
brew install git-secrets

# Instalação no Linux
git clone https://github.com/awslabs/git-secrets.git
cd git-secrets
sudo make install

# Configuração inicial
git secrets --install
git secrets --register-aws

4.2. Definindo padrões de busca (regex) e arquivos permitidos

# Adicionar padrões personalizados
git secrets --add 'senha\s*=\s*["\x27]?[A-Za-z0-9!@#$%^&*()_+=-]{8,}["\x27]?'
git secrets --add 'api_key\s*[:=]\s*["\x27][A-Za-z0-9]{20,}["\x27]'

# Adicionar provedores (AWS, GCP, etc.)
git secrets --add-provider -- cat ~/.git-secrets-providers/aws.txt

# Permitir arquivos específicos (ex: testes que usam tokens mock)
git secrets --add --allowed 'test_token_12345'

4.3. Integração com pre-commit hook para execução automática

# O comando git secrets --install já cria o hook automaticamente
# Verifique se está funcionando:
cat .git/hooks/pre-commit

# Saída esperada:
#!/bin/sh
git secrets --pre_commit_hook -- "$@"

Teste a configuração:

# Criar um arquivo de teste com credencial falsa
echo "password=super_secreta_123" > test_creds.txt
git add test_creds.txt
git commit -m "teste"  # Deve falhar

5. Remediation: Removendo Credenciais do Histórico

5.1. Usando git filter-branch para reescrever o histórico

Se uma credencial já foi commitada, use git filter-branch:

# Remover um arquivo específico de todo o histórico
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch config/credentials.json" \
  --prune-empty --tag-name-filter cat -- --all

# Substituir texto sensível em todo o histórico
git filter-branch --force --tree-filter \
  "sed -i 's/senha_real_123/REMOVED/g' *.env" \
  --prune-empty --tag-name-filter cat -- --all

5.2. Alternativa moderna: git filter-repo (BFG Repo-Cleaner)

O git filter-repo é mais rápido e seguro:

# Instalação
pip install git-filter-repo

# Remover arquivo específico
git filter-repo --path config/credentials.json --invert-paths

# Substituir texto em todo o histórico
git filter-repo --replace-text <(echo "senha_real_123==>REMOVED")

# Remover arquivos por padrão
git filter-repo --path-glob '*.pem' --invert-paths

5.3. Impacto em repositórios compartilhados e git push --force

Após reescrever o histórico, todos os colaboradores precisam:

# No repositório local de cada desenvolvedor
git fetch --all
git reset --hard origin/main

# Forçar push para o remoto (com cuidado!)
git push --force --all
git push --force --tags

ATENÇÃO: Notifique toda a equipe antes de forçar o push. Idealmente, bloqueie o repositório durante a operação.

6. Políticas Server-Side com Pre-Receive Hooks

6.1. Validando novos commits no servidor remoto

No GitHub, GitLab ou servidores auto-hospedados, configure hooks server-side para rejeitar commits com credenciais antes que cheguem ao repositório central.

6.2. Script de rejeição para padrões de credenciais

Exemplo de pre-receive hook para servidor Git:

#!/bin/bash
# pre-receive hook para rejeitar credenciais

zero_commit="0000000000000000000000000000000000000000"

while read oldrev newrev refname; do
    # Lista todos os commits novos
    for commit in $(git rev-list $oldrev..$newrev); do
        # Verifica diff do commit
        if git diff-tree --no-commit-id -r $commit | grep -qE '(password|secret|token|key)\s*[:=]'; then
            echo "❌ REJEITADO: Credencial detectada no commit $commit"
            echo "   Remova a credencial e faça um novo commit."
            exit 1
        fi
    done
done

exit 0

6.3. Mensagens de erro amigáveis e instruções para o desenvolvedor

echo "╔══════════════════════════════════════════════════════════════╗"
echo "║  ❌ COMMIT REJEITADO: Credenciais detectadas               ║"
echo "║                                                            ║"
echo "║  Seu commit contém padrões que parecem ser credenciais.    ║"
echo "║                                                            ║"
echo "║  Para corrigir:                                            ║"
echo "║  1. Use git revert no último commit                        ║"
echo "║  2. Remova as credenciais dos arquivos                     ║"
echo "║  3. Adicione os arquivos ao .gitignore                     ║"
echo "║  4. Faça um novo commit                                    ║"
echo "║                                                            ║"
echo "║  Dica: Use 'git secrets' para verificar localmente         ║"
echo "╚══════════════════════════════════════════════════════════════╝"

7. Boas Práticas e Automação Contínua

7.1. Template de repositório com hooks e .gitignore pré-configurados

Crie um template de repositório no GitHub/GitLab com:

meu-template/
├── .githooks/
│   └── pre-commit        # Script de verificação
├── .gitignore            # Com padrões de segurança
├── setup.sh              # Script de configuração automática
└── README.md

7.2. Integração com CI/CD para escanear branches antes do merge

No GitHub Actions, adicione um workflow:

name: Secret Scanning

on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Install git-secrets
        run: |
          git clone https://github.com/awslabs/git-secrets.git
          cd git-secrets && sudo make install
      - name: Run git-secrets
        run: |
          git secrets --scan-history

7.3. Cultura de equipe: checklist de segurança para commits

Checklist para cada desenvolvedor antes do commit:

  • [ ] Revisei os arquivos staged (git diff --cached)
  • [ ] Não há senhas, tokens ou chaves nos arquivos
  • [ ] Arquivos com credenciais estão no .gitignore
  • [ ] Executei git secrets --scan (se instalado)
  • [ ] Testei o pre-commit hook localmente

Referências