Pular para o conteúdo

Tirar segredos do Git em GitOps

Avançado16 min de leituraseguranca

GitOps exige que o estado desejado esteja no Git. Segredo não pode estar. O External Secrets Operator resolve a contradição: o repositório guarda uma referência, e o operador busca o valor no cofre e mantém o Secret sincronizado no cluster.

1. Entenda por que segredo cifrado no Git incomoda

Seção intitulada “1. Entenda por que segredo cifrado no Git incomoda”

Sealed Secrets e SOPS funcionam e são melhores que texto em claro. Mas:

  • O ciphertext fica no histórico para sempre; se a chave vazar depois, todo o histórico é legível.
  • Rotacionar exige commit, PR e deploy — o que faz ninguém rotacionar.
  • A auditoria de “quem leu este segredo” não existe.
  • Recuperação de desastre depende de guardar bem a chave mestra, que vira o novo problema.

Com cofre + operador, você ganha rotação sem commit, trilha de auditoria por acesso e controle de permissão centralizado.

Janela do terminal
helm repo add external-secrets https://charts.external-secrets.io
helm upgrade --install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace --version 0.10.4
kubectl get pods -n external-secrets

O ponto crítico: não use uma credencial estática para o operador falar com o cofre — isso só mudaria o segredo de lugar. Use a identidade da ServiceAccount.

# AWS Secrets Manager via IRSA
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata: { name: aws-prod }
spec:
provider:
aws:
service: SecretsManager
region: sa-east-1
auth:
jwt:
serviceAccountRef: { name: external-secrets, namespace: external-secrets }
# Vault com autenticação Kubernetes
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata: { name: vault-prod }
spec:
provider:
vault:
server: https://vault.exemplo.com.br
path: kv
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: external-secrets
serviceAccountRef: { name: external-secrets, namespace: external-secrets }
Janela do terminal
kubectl get clustersecretstore aws-prod -o jsonpath='{.status.conditions}' | jq
# Ready=True antes de seguir

Este arquivo vai para o Git. Ele não contém valor nenhum.

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: loja-db, namespace: loja }
spec:
refreshInterval: 1h
secretStoreRef: { name: aws-prod, kind: ClusterSecretStore }
target:
name: loja-db # Secret criado no cluster
creationPolicy: Owner # apagou o ExternalSecret, apaga o Secret
template:
type: Opaque
data: # monte a string de conexão sem expor as partes
DATABASE_URL: "postgres://{{ .usuario }}:{{ .senha }}@postgres.loja:5432/pedidos"
data:
- secretKey: usuario
remoteRef: { key: prod/loja/postgres, property: usuario }
- secretKey: senha
remoteRef: { key: prod/loja/postgres, property: senha }
Janela do terminal
kubectl get externalsecret loja-db -n loja # SYNCED=True
kubectl get secret loja-db -n loja # criado pelo operador, não pelo Git

Rotacionar passa a ser uma operação no cofre. O operador propaga em até refreshInterval.

Janela do terminal
aws secretsmanager put-secret-value --secret-id prod/loja/postgres \
--secret-string '{"usuario":"loja","senha":"NOVA_SENHA"}'

Falta um detalhe que costuma ser esquecido: a aplicação precisa reler o valor. Variável de ambiente é lida uma vez no boot. Duas saídas:

# 1. o Reloader (ou equivalente) reinicia o Deployment quando o Secret muda
metadata:
annotations: { reloader.stakater.com/auto: "true" }
# 2. melhor: monte como arquivo e faça a aplicação reler
volumeMounts: [{ name: credenciais, mountPath: /etc/segredos, readOnly: true }]
volumes: [{ name: credenciais, secret: { secretName: loja-db } }]

Secret montado como volume é atualizado no Pod sem reinício — mas a aplicação precisa reabrir o arquivo.

O operador tira o segredo do Git; ele não protege o cluster sozinho:

  • RBAC: negue get secrets por padrão; quem lê o Secret tem a senha do banco.
  • Criptografia em repouso do etcd, com KMS externo.
  • Cofre com permissão mínima: o ClusterSecretStore de produção não deve enxergar os caminhos de outros ambientes; considere um SecretStore por namespace.
  • Auditoria: alerta em leituras fora do padrão no cofre.
Sintoma Causa provável
SecretSyncedError Caminho ou property errado no remoteRef
InvalidProviderConfig ServiceAccount sem permissão no cofre (IRSA/role)
Secret criado vazio property inexistente no JSON armazenado
Aplicação usa a senha antiga Faltou reiniciar ou reler o arquivo
Argo CD marca OutOfSync O Secret gerado não está no Git: ignore-o na diff
Segredo some ao remover o manifest creationPolicy: Owner — é o comportamento esperado
  • Operador instalado e ClusterSecretStore com Ready=True.
  • Autenticação por identidade de workload, sem credencial estática.
  • Nenhum valor de segredo no repositório (varredura confirma).
  • ExternalSecret sincronizando, com refreshInterval definido.
  • Estratégia de recarga da aplicação escolhida e testada.
  • RBAC restringindo leitura de Secrets.
  • Rotação exercitada de ponta a ponta.